Repository: favonia/cloudflare-ddns
Pull request #867 (2024-08-11 19:11:57)

refactor: minor misc code refactoring

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/867?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) Report
Attention: Patch coverage is `87.03704%` with `14 lines` in your changes missing coverage. Please review.
> Project coverage is 94.48%. Comparing base [(`a9b08e6`)](https://app.codecov.io/gh/favonia/cloudflare-ddns/commit/a9b08e6a75c4243bb7c59c25dd2b8f38fb7419d5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) to head [(`c640379`)](https://app.codecov.io/gh/favonia/cloudflare-ddns/commit/c640379a2684577512f8bedd145baba980ab30d6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia).

| [Files](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/867?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) | Patch % | Lines |
|---|---|---|
| [cmd/ddns/ddns.go](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/867?src=pr&el=tree&filepath=cmd%2Fddns%2Fddns.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-Y21kL2RkbnMvZGRucy5nbw==) | 0.00% | [11 Missing :warning: ](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/867?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) |
| [internal/api/cloudflare\_waf.go](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/867?src=pr&el=tree&filepath=internal%2Fapi%2Fcloudflare_waf.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-aW50ZXJuYWwvYXBpL2Nsb3VkZmxhcmVfd2FmLmdv) | 80.00% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/867?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) |

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


```diff
@@            Coverage Diff             @@
##             main     #867      +/-   ##
==========================================
- Coverage   94.70%   94.48%   -0.23%     
==========================================
  Files          58       58              
  Lines        2569     2591      +22     
==========================================
+ Hits         2433     2448      +15     
- Misses        123      129       +6     
- Partials       13       14       +1     
```

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

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

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/867?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 cbc4bed..3086daf 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -49,6 +49,7 @@ linters:
     - 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
+    - nestif # can detect complicated code, but never leads to actual code changes
 
     - depguard # useless; I do not introduce a dependency carelessly
     - gosmopolitan # interesting for i18n checking, useless for this project
diff --git a/cmd/ddns/ddns.go b/cmd/ddns/ddns.go
index de99023..aa408dc 100644
--- a/cmd/ddns/ddns.go
+++ b/cmd/ddns/ddns.go
@@ -28,7 +28,7 @@ func formatName() string {
 	return fmt.Sprintf("Cloudflare DDNS (%s)", Version)
 }
 
-func initConfig(ctx context.Context, ppfmt pp.PP) (*config.Config, setter.Setter, bool) {
+func initConfig(ppfmt pp.PP) (*config.Config, setter.Setter, bool) {
 	c := config.Default()
 
 	// Read the config
@@ -40,7 +40,7 @@ func initConfig(ctx context.Context, ppfmt pp.PP) (*config.Config, setter.Setter
 	c.Print(ppfmt)
 
 	// Get the handler
-	h, ok := c.Auth.New(ctx, ppfmt, c.CacheExpiration)
+	h, ok := c.Auth.New(ppfmt, c.CacheExpiration)
 	if !ok {
 		return c, nil, false
 	}
@@ -87,11 +87,11 @@ func realMain() int { //nolint:funlen
 	config.CheckRoot(ppfmt)
 
 	// Read the config and get the handler and the setter
-	c, s, configOk := initConfig(ctx, ppfmt)
+	c, s, configOK := initConfig(ppfmt)
 	// Ping monitors regardless of whether initConfig succeeded
 	monitor.StartAll(ctx, ppfmt, c.Monitors, formatName())
 	// Bail out now if initConfig failed
-	if !configOk {
+	if !configOK {
 		monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 1, "Configuration errors")
 		notifier.SendAll(ctx, ppfmt, c.Notifiers,
 			"Cloudflare DDNS was misconfigured and could not start. Please check the logging for details.")
@@ -124,13 +124,19 @@ func realMain() int { //nolint:funlen
 		if first && !c.UpdateOnStart {
 			monitor.SuccessAll(ctx, ppfmt, c.Monitors, "Started (no action)")
 		} else {
-			if c.UpdateCron != nil && !s.SanityCheck(ctx, ppfmt) {
-				monitor.SuccessAll(ctx, ppfmt, c.Monitors, "Invalid Cloudflare API token")
-				notifier.SendAll(ctx, ppfmt, c.Notifiers,
-					"The Cloudflare API token is invalid. "+
-						"Please check the value of CF_API_TOKEN or CF_API_TOKEN_FILE.",
-				)
-				return 1
+			if c.UpdateCron != nil { // no need to do sanity check if it's a one-time update
+				if ok, certain := s.SanityCheck(ctxWithSignals, ppfmt); !ok && certain {
+					monitor.FailureAll(ctx, ppfmt, c.Monitors, "Invalid Cloudflare API token")
+					notifier.SendAll(ctx, ppfmt, c.Notifiers,
+						"The Cloudflare API token is invalid. "+
+							"Please check the value of CF_API_TOKEN or CF_API_TOKEN_FILE.",
+					)
+					return 1
+				}
+			}
+
+			if ctxWithSignals.Err() != nil {
+				goto signaled
 			}
 
 			msg := updater.UpdateIPs(ctxWithSignals, ppfmt, c, s)
@@ -138,6 +144,10 @@ func realMain() int { //nolint:funlen
 			notifier.SendMessageAll(ctx, ppfmt, c.Notifiers, msg)
 		}
 
+		if ctxWithSignals.Err() != nil {
+			goto signaled
+		}
+
 		// Check if cron was disabled
 		if c.UpdateCron == nil {
 			ppfmt.Infof(pp.EmojiBye, "Bye!")
@@ -168,8 +178,9 @@ func realMain() int { //nolint:funlen
 		// Display the remaining time interval
 		cron.PrintCountdown(ppfmt, "Checking the IP addresses", time.Now(), next)
 
+	signaled:
 		// Wait for the next signal or the alarm, whichever comes first
-		if !sig.SleepUntil(ppfmt, next) {
+		if sig.ReportSignalsUntil(ppfmt, next) {
 			stopUpdating(ctx, ppfmt, c, s)
 			monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 0, "Stopped")
 			if c.UpdateCron != nil {
diff --git a/internal/api/base.go b/internal/api/base.go
index e208d01..17f0b76 100644
--- a/internal/api/base.go
+++ b/internal/api/base.go
@@ -29,8 +29,9 @@ type WAFListItem struct {
 // Currently, the only implementation is Cloudflare.
 type Handle interface {
 	// 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
+	// The first return value indicates whether it passes the test.
+	// The second return value indicates whether we are certain about the result.
+	SanityCheck(ctx context.Context, ppfmt pp.PP) (bool, bool)
 
 	// ListRecords lists all matching DNS records.
 	//
@@ -70,7 +71,7 @@ type Handle interface {
 // An Auth contains authentication information.
 type Auth interface {
 	// New uses the authentication information to create a Handle.
-	New(ctx context.Context, ppfmt pp.PP, cacheExpiration time.Duration) (Handle, bool)
+	New(ppfmt pp.PP, cacheExpiration time.Duration) (Handle, bool)
 
 	// Check whether DNS records are supported.
 	SupportsRecords() bool
diff --git a/internal/api/cloudflare.go b/internal/api/cloudflare.go
index 58f626d..44ff508 100644
--- a/internal/api/cloudflare.go
+++ b/internal/api/cloudflare.go
@@ -8,14 +8,22 @@ import (
 	"github.com/cloudflare/cloudflare-go"
 	"github.com/jellydator/ttlcache/v3"
 
+	"github.com/favonia/cloudflare-ddns/internal/cron"
 	"github.com/favonia/cloudflare-ddns/internal/ipnet"
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
 
+type sanityCheckType int
+
+const (
+	sanityCheckToken sanityCheckType = iota
+	sanityCheckAccount
+)
+
 // CloudflareCache holds the previous repsonses from the Cloudflare API.
 type CloudflareCache = struct {
 	// sanity check
-	sanityCheck *ttlcache.Cache[struct{}, bool] // whether token is valid
+	sanityCheck *ttlcache.Cache[sanityCheckType, bool] // whether token or account is valid
 	// domains to zones
 	listZones    *ttlcache.Cache[string, []string] // zone names to zone IDs
 	zoneOfDomain *ttlcache.Cache[string, string]   // domain names to the zone ID
@@ -52,7 +60,7 @@ type CloudflareAuth struct {
 }
 
 // New creates a [CloudflareHandle] from the authentication data.
-func (t CloudflareAuth) New(_ context.Context, ppfmt pp.PP, cacheExpiration time.Duration) (Handle, bool) {
+func (t CloudflareAuth) New(ppfmt pp.PP, cacheExpiration time.Duration) (Handle, bool) {
 	handle, err := cloudflare.NewWithAPIToken(t.Token)
 	if err != nil {
 		ppfmt.Errorf(pp.EmojiUserError, "Failed to prepare the Cloudflare authentication: %v", err)
@@ -68,7 +76,7 @@ func (t CloudflareAuth) New(_ context.Context, ppfmt pp.PP, cacheExpiration time
 		cf:        handle,
 		accountID: t.AccountID,
 		cache: CloudflareCache{
-			sanityCheck:  newCache[struct{}, bool](cacheExpiration),
+			sanityCheck:  newCache[sanityCheckType, bool](cacheExpiration),
 			listZones:    newCache[string, []string](cacheExpiration),
 			zoneOfDomain: newCache[string, string](cacheExpiration),
 			listRecords: map[ipnet.Type]*ttlcache.Cache[string, *[]Record]{
@@ -108,60 +116,84 @@ func (h CloudflareHandle) FlushCache() {
 // errTimeout for checking if it's timeout.
 var errTimeout = errors.New("timeout")
 
-// SanityCheck verifies Cloudflare tokens.
-//
-// Ideally, we should also verify accountID here, but that is impossible without
-// more permissions included in the API token.
-func (h CloudflareHandle) SanityCheck(ctx context.Context, ppfmt pp.PP) bool {
-	if valid := h.cache.sanityCheck.Get(struct{}{}); valid != nil {
-		return valid.Value()
+func (h CloudflareHandle) skipSanityCheckToken() {
+	h.cache.sanityCheck.Set(sanityCheckToken, true, ttlcache.DefaultTTL)
+}
+
+func (h CloudflareHandle) skipSanityCheck() {
+	h.skipSanityCheckToken()
+	h.cache.sanityCheck.Set(sanityCheckAccount, true, ttlcache.DefaultTTL)
+}
+
+// SanityCheckToken verifies the Cloudflare token.
+func (h CloudflareHandle) SanityCheckToken(ctx context.Context, ppfmt pp.PP) (bool, bool) {
+	if valid := h.cache.sanityCheck.Get(sanityCheckToken); valid != nil {
+		return valid.Value(), true
 	}
 
 	quickCtx, cancel := context.WithTimeoutCause(ctx, time.Second, errTimeout)
 	defer cancel()
 
-	ok := true
 	res, err := h.cf.VerifyAPIToken(quickCtx)
 	if err != nil {
-		// Check if the token is permanently invalid...
-		var aerr *cloudflare.AuthorizationError
-		var rerr *cloudflare.RequestError
-		if errors.As(err, &aerr) || errors.As(err, &rerr) {
-			ppfmt.Errorf(pp.EmojiUserError, "The Cloudflare API token is invalid: %v", err)
-			ok = false
-			goto permanently
+		if quickCtx.Err() != nil {
+			return true, false
 		}
-		if !errors.Is(context.Cause(quickCtx), errTimeout) {
-			ppfmt.Warningf(pp.EmojiWarning, "Failed to verify the Cloudflare API token; will retry later: %v", err)
+
+		var requestError *cloudflare.RequestError
+		var authorizationError *cloudflare.AuthorizationError
+
+		// known error messages
+		// 400:6003:"Invalid request headers"
+		// 400:6111:"Invalid format for Authorization header"
+		// 401:1000:"Invalid API Token"
+
+		switch {
+		case errors.As(err, &requestError),
+			errors.As(err, &requestError) && requestError.InternalErrorCodeIs(6111),             //nolint:mnd
+			errors.As(err, &authorizationError) && authorizationError.InternalErrorCodeIs(1000): //nolint:mnd
+
+			ppfmt.Errorf(pp.EmojiUserError,
+				"The Cloudflare API token is invalid; "+
+					"please check the value of CF_API_TOKEN or CF_API_TOKEN_FILE")
+			goto certainlyBad
+
+		default:
+			// We will try again later.
+			return true, false
 		}
-		return true // It could be that the network is temporarily down.
 	}
+
+	// The API call succeeded, but the token might be in a bad status.
 	switch res.Status {
 	case "active":
 	case "disabled", "expired":
 		ppfmt.Errorf(pp.EmojiUserError, "The Cloudflare API token is %s", res.Status)
-		ok = false
-		goto permanently
+		goto certainlyBad
 	default:
 		ppfmt.Warningf(pp.EmojiImpossible,
 			"The Cloudflare API token is in an undocumented state %q; please report this at %s",
 			res.Status, pp.IssueReportingURL)
-		goto permanently
+		return true, false
 	}
 
 	if !res.ExpiresOn.IsZero() {
-		ppfmt.Warningf(pp.EmojiAlarm, "The token will expire at %s",
-			res.ExpiresOn.In(time.Local).Format(time.RFC1123Z))
-	}
+		now := time.Now()
+		remainingLifespan := max(res.ExpiresOn.Sub(now), 0)
 
-permanently:
-	if !ok {
-		ppfmt.Errorf(pp.EmojiUserError, "Please double-check the value of CF_API_TOKEN or CF_API_TOKEN_FILE")
+		ppfmt.Warningf(pp.EmojiAlarm, "The Cloudflare API token will expire at %s (%v left)",
+			cron.DescribeIntuitively(now, res.ExpiresOn), remainingLifespan)
 	}
-	h.cache.sanityCheck.Set(struct{}{}, ok, ttlcache.DefaultTTL)
-	return ok
+
+	h.cache.sanityCheck.Set(sanityCheckToken, true, ttlcache.DefaultTTL)
+	return true, true
+
+certainlyBad:
+	return false, true
 }
 
-func (h CloudflareHandle) forcePassSanityCheck() {
-	h.cache.sanityCheck.Set(struct{}{}, true, ttlcache.DefaultTTL)
+// SanityCheck verifies both the Cloudflare API token and account ID.
+// It returns false only when the token or the account ID is certainly bad.
+func (h CloudflareHandle) SanityCheck(ctx context.Context, ppfmt pp.PP) (bool, bool) {
+	return h.SanityCheckToken(ctx, ppfmt)
 }
diff --git a/internal/api/cloudflare_records.go b/internal/api/cloudflare_records.go
index 29b9ff2..0053e28 100644
--- a/internal/api/cloudflare_records.go
+++ b/internal/api/cloudflare_records.go
@@ -32,7 +32,7 @@ func (h CloudflareHandle) ListZones(ctx context.Context, ppfmt pp.PP, name strin
 	}
 
 	// The operation went through. No need to perform any sanity checking in near future!
-	h.forcePassSanityCheck()
+	h.skipSanityCheckToken()
 
 	ids := make([]string, 0, len(res.Result))
 	for _, zone := range res.Result {
@@ -80,7 +80,7 @@ zoneSearch:
 		}
 
 		// The operation went through. No need to perform any sanity checking in near future!
-		h.forcePassSanityCheck()
+		h.skipSanityCheckToken()
 
 		switch len(zones) {
 		case 0: // len(zones) == 0
@@ -119,7 +119,7 @@ func (h CloudflareHandle) ListRecords(ctx context.Context, ppfmt pp.PP,
 	}
 
 	// The operation went through. No need to perform any sanity checking in near future!
-	h.forcePassSanityCheck()
+	h.skipSanityCheckToken()
 
 	//nolint:exhaustruct // Other fields are intentionally unspecified
 	raw, _, err := h.cf.ListDNSRecords(ctx,
@@ -173,7 +173,7 @@ func (h CloudflareHandle) DeleteRecord(ctx context.Context, ppfmt pp.PP,
 	}
 
 	// The operation went through. No need to perform any sanity checking in near future!
-	h.forcePassSanityCheck()
+	h.skipSanityCheckToken()
 
 	if rs := h.cache.listRecords[ipNet].Get(domain.DNSNameASCII()); rs != nil {
 		*rs.Value() = slices.DeleteFunc(*rs.Value(), func(r Record) bool { return r.ID == id })
@@ -207,7 +207,7 @@ func (h CloudflareHandle) UpdateRecord(ctx context.Context, ppfmt pp.PP,
 	}
 
 	// The operation went through. No need to perform any sanity checking in near future!
-	h.forcePassSanityCheck()
+	h.skipSanityCheckToken()
 
 	if rs := h.cache.listRecords[ipNet].Get(domain.DNSNameASCII()); rs != nil {
 		for i, r := range *rs.Value() {
@@ -250,7 +250,7 @@ func (h CloudflareHandle) CreateRecord(ctx context.Context, ppfmt pp.PP,
 	}
 
 	// The operation went through. No need to perform any sanity checking in near future!
-	h.forcePassSanityCheck()
+	h.skipSanityCheckToken()
 
 	if rs := h.cache.listRecords[ipNet].Get(domain.DNSNameASCII()); rs != nil {
 		*rs.Value() = append([]Record{{ID: res.ID, IP: ip}}, *rs.Value()...)
diff --git a/internal/api/cloudflare_waf.go b/internal/api/cloudflare_waf.go
index 986c375..f957d3a 100644
--- a/internal/api/cloudflare_waf.go
+++ b/internal/api/cloudflare_waf.go
@@ -2,6 +2,7 @@ package api
 
 import (
 	"context"
+	"errors"
 	"net/netip"
 
 	"github.com/cloudflare/cloudflare-go"
@@ -25,6 +26,14 @@ var WAFListMaxBitLen = map[ipnet.Type]int{ //nolint:gochecknoglobals
 	ipnet.IP6: 64, //nolint:mnd
 }
 
+func hintTokenWAFPermission(ppfmt pp.PP, err error) {
+	var authentication *cloudflare.AuthenticationError
+	if errors.As(err, &authentication) {
+		ppfmt.Hintf(pp.HintCloudflareWAFPermissions,
+			`Make sure you granted the "Edit" permission of "Account - Account Filter Lists"`)
+	}
+}
+
 // ListWAFLists lists all IP lists of the given name.
 func (h CloudflareHandle) ListWAFLists(ctx context.Context, ppfmt pp.PP) (map[string]string, bool) {
 	if lmap := h.cache.listLists.Get(struct{}{}); lmap != nil {
@@ -34,9 +43,11 @@ func (h CloudflareHandle) ListWAFLists(ctx context.Context, ppfmt pp.PP) (map[st
 	ls, err := h.cf.ListLists(ctx, cloudflare.AccountIdentifier(h.accountID), cloudflare.ListListsParams{})
 	if err != nil {
 		ppfmt.Warningf(pp.EmojiError, "Failed to list existing lists: %v", err)
+		hintTokenWAFPermission(ppfmt, err)
 		return nil, false
 	}
-	h.forcePassSanityCheck()
+
+	h.skipSanityCheck()
 
 	lmap := map[string]string{}
 	for _, l := range ls {
@@ -100,10 +111,12 @@ func (h CloudflareHandle) EnsureWAFList(ctx context.Context, ppfmt pp.PP,
 		})
 	if err != nil {
 		ppfmt.Warningf(pp.EmojiError, "Failed to create a list named %q: %v", listName, err)
+		hintTokenWAFPermission(ppfmt, err)
 		h.cache.listLists.Delete(struct{}{})
 		return "", false, false
 	}
-	h.forcePassSanityCheck()
+
+	h.skipSanityCheck()
 
 	if lmap := h.cache.listLists.Get(struct{}{}); lmap != nil {
 		lmap.Value()[listName] = r.ID
@@ -121,10 +134,12 @@ func (h CloudflareHandle) DeleteWAFList(ctx context.Context, ppfmt pp.PP, listNa
 
 	if _, err := h.cf.DeleteList(ctx, cloudflare.AccountIdentifier(h.accountID), listID); err != nil {
 		ppfmt.Warningf(pp.EmojiError, "Failed to delete the list %q: %v", listName, err)
+		hintTokenWAFPermission(ppfmt, err)
 		h.cache.listLists.Delete(struct{}{})
 		return false
 	}
-	h.forcePassSanityCheck()
+
+	h.skipSanityCheck()
 
 	if lmap := h.cache.listLists.Get(struct{}{}); lmap != nil {
 		delete(lmap.Value(), listName)
@@ -167,10 +182,11 @@ func (h CloudflareHandle) ListWAFListItems(ctx context.Context, ppfmt pp.PP, lis
 	)
 	if err != nil {
 		ppfmt.Warningf(pp.EmojiError, "Failed to retrieve items in the list %q (ID: %s): %v", listName, listID, err)
+		hintTokenWAFPermission(ppfmt, err)
 		return nil, false, false
 	}
 
-	h.forcePassSanityCheck()
+	h.skipSanityCheck()
 
 	items, ok := readWAFListItems(ppfmt, listName, listID, rawItems)
 	if !ok {
@@ -207,11 +223,12 @@ func (h CloudflareHandle) DeleteWAFListItems(ctx context.Context, ppfmt pp.PP, l
 	)
 	if err != nil {
 		ppfmt.Warningf(pp.EmojiError, "Failed to finish deleting items from the list %q (ID: %s): %v", listName, listID, err)
+		hintTokenWAFPermission(ppfmt, err)
 		h.cache.listListItems.Delete(listID)
 		return false
 	}
 
-	h.forcePassSanityCheck()
+	h.skipSanityCheck()
 
 	items, ok := readWAFListItems(ppfmt, listName, listID, rawItems)
 	if !ok {
@@ -256,11 +273,12 @@ func (h CloudflareHandle) CreateWAFListItems(ctx context.Context, ppfmt pp.PP,
 		ppfmt.Warningf(
 			pp.EmojiError, "Failed to finish adding items to the list %q (ID: %s): %v",
 			listName, listID, err)
+		hintTokenWAFPermission(ppfmt, err)
 		h.cache.listListItems.Delete(listID)
 		return false
 	}
 
-	h.forcePassSanityCheck()
+	h.skipSanityCheck()
 
 	items, ok := readWAFListItems(ppfmt, listName, listID, rawItems)
 	if !ok {
diff --git a/internal/message/message.go b/internal/message/message.go
index 1981b7a..a0ed257 100644
--- a/internal/message/message.go
+++ b/internal/message/message.go
@@ -6,7 +6,7 @@ package message
 // To monitors, the status is far more important than the message,
 // and to notifiers, all messages are important.
 type Message struct {
-	Ok               bool
+	OK               bool
 	MonitorMessages  []string
 	NotifierMessages []string
 }
@@ -14,7 +14,7 @@ type Message struct {
 // NewEmpty creates a new empty Message.
 func NewEmpty() Message {
 	return Message{
-		Ok:               true,
+		OK:               true,
 		MonitorMessages:  nil,
 		NotifierMessages: nil,
 	}
@@ -25,20 +25,20 @@ func NewEmpty() Message {
 // - For notifiers, we collect all the messages.
 func Merge(msgs ...Message) Message {
 	var (
-		allOk                        = true
+		allOK                        = true
 		allMonitorMessages           = map[bool][]string{}
 		allNotifierMessages []string = nil
 	)
 
 	for _, msg := range msgs {
-		allOk = allOk && msg.Ok
-		allMonitorMessages[msg.Ok] = append(allMonitorMessages[msg.Ok], msg.MonitorMessages...)
+		allOK = allOK && msg.OK
+		allMonitorMessages[msg.OK] = append(allMonitorMessages[msg.OK], msg.MonitorMessages...)
 		allNotifierMessages = append(allNotifierMessages, msg.NotifierMessages...)
 	}
 
 	return Message{
-		Ok:               allOk,
-		MonitorMessages:  allMonitorMessages[allOk],
+		OK:               allOK,
+		MonitorMessages:  allMonitorMessages[allOK],
 		NotifierMessages: allNotifierMessages,
 	}
 }
diff --git a/internal/mocks/mock_api.go b/internal/mocks/mock_api.go
index dcee2c0..5d583da 100644
--- a/internal/mocks/mock_api.go
+++ b/internal/mocks/mock_api.go
@@ -355,11 +355,12 @@ func (c *HandleListWAFListItemsCall) DoAndReturn(f func(context.Context, pp.PP,
 }
 
 // SanityCheck mocks base method.
-func (m *MockHandle) SanityCheck(arg0 context.Context, arg1 pp.PP) bool {
+func (m *MockHandle) SanityCheck(arg0 context.Context, arg1 pp.PP) (bool, bool) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "SanityCheck", arg0, arg1)
 	ret0, _ := ret[0].(bool)
-	return ret0
+	ret1, _ := ret[1].(bool)
+	return ret0, ret1
 }
 
 // SanityCheck indicates an expected call of SanityCheck.
@@ -375,19 +376,19 @@ type HandleSanityCheckCall struct {
 }
 
 // Return rewrite *gomock.Call.Return
-func (c *HandleSanityCheckCall) Return(arg0 bool) *HandleSanityCheckCall {
-	c.Call = c.Call.Return(arg0)
+func (c *HandleSanityCheckCall) Return(arg0, arg1 bool) *HandleSanityCheckCall {
+	c.Call = c.Call.Return(arg0, arg1)
 	return c
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *HandleSanityCheckCall) Do(f func(context.Context, pp.PP) bool) *HandleSanityCheckCall {
+func (c *HandleSanityCheckCall) Do(f func(context.Context, pp.PP) (bool, bool)) *HandleSanityCheckCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *HandleSanityCheckCall) DoAndReturn(f func(context.Context, pp.PP) bool) *HandleSanityCheckCall {
+func (c *HandleSanityCheckCall) DoAndReturn(f func(context.Context, pp.PP) (bool, bool)) *HandleSanityCheckCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
diff --git a/internal/mocks/mock_setter.go b/internal/mocks/mock_setter.go
index e7562bc..aec5e40 100644
--- a/internal/mocks/mock_setter.go
+++ b/internal/mocks/mock_setter.go
@@ -121,11 +121,12 @@ func (c *SetterDeleteWAFListCall) DoAndReturn(f func(context.Context, pp.PP, str
 }
 
 // SanityCheck mocks base method.
-func (m *MockSetter) SanityCheck(arg0 context.Context, arg1 pp.PP) bool {
+func (m *MockSetter) SanityCheck(arg0 context.Context, arg1 pp.PP) (bool, bool) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "SanityCheck", arg0, arg1)
 	ret0, _ := ret[0].(bool)
-	return ret0
+	ret1, _ := ret[1].(bool)
+	return ret0, ret1
 }
 
 // SanityCheck indicates an expected call of SanityCheck.
@@ -141,19 +142,19 @@ type SetterSanityCheckCall struct {
 }
 
 // Return rewrite *gomock.Call.Return
-func (c *SetterSanityCheckCall) Return(arg0 bool) *SetterSanityCheckCall {
-	c.Call = c.Call.Return(arg0)
+func (c *SetterSanityCheckCall) Return(arg0, arg1 bool) *SetterSanityCheckCall {
+	c.Call = c.Call.Return(arg0, arg1)
 	return c
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *SetterSanityCheckCall) Do(f func(context.Context, pp.PP) bool) *SetterSanityCheckCall {
+func (c *SetterSanityCheckCall) Do(f func(context.Context, pp.PP) (bool, bool)) *SetterSanityCheckCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *SetterSanityCheckCall) DoAndReturn(f func(context.Context, pp.PP) bool) *SetterSanityCheckCall {
+func (c *SetterSanityCheckCall) DoAndReturn(f func(context.Context, pp.PP) (bool, bool)) *SetterSanityCheckCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
diff --git a/internal/monitor/base.go b/internal/monitor/base.go
index db2bb6e..e8b7781 100644
--- a/internal/monitor/base.go
+++ b/internal/monitor/base.go
@@ -39,7 +39,7 @@ type Monitor interface {
 // PingMessage formats and pings with a [message.Message].
 func PingMessage(ctx context.Context, ppfmt pp.PP, m Monitor, msg message.Message) bool {
 	monitorMsg := strings.Join(msg.MonitorMessages, "\n")
-	if msg.Ok {
+	if msg.OK {
 		return m.Success(ctx, ppfmt, monitorMsg)
 	} else {
 		return m.Failure(ctx, ppfmt, monitorMsg)
@@ -50,7 +50,7 @@ func PingMessage(ctx context.Context, ppfmt pp.PP, m Monitor, msg message.Messag
 func LogMessage(ctx context.Context, ppfmt pp.PP, m Monitor, msg message.Message) bool {
 	monitorMsg := strings.Join(msg.MonitorMessages, "\n")
 	switch {
-	case !msg.Ok:
+	case !msg.OK:
 		return m.Failure(ctx, ppfmt, monitorMsg)
 	case len(msg.MonitorMessages) > 0:
 		return m.Log(ctx, ppfmt, monitorMsg)
diff --git a/internal/monitor/uptimekuma.go b/internal/monitor/uptimekuma.go
index 707bfde..e5b9bab 100644
--- a/internal/monitor/uptimekuma.go
+++ b/internal/monitor/uptimekuma.go
@@ -63,7 +63,7 @@ func NewUptimeKuma(ppfmt pp.PP, rawURL string) (UptimeKuma, bool) {
 
 	// By default, the URL provided by Uptime Kuma has this:
 	//
-	//     https://some.host.name/api/push/GFWB6vsHMg?status=up&msg=Ok&ping=
+	//     https://some.host.name/api/push/GFWB6vsHMg?status=up&msg=OK&ping=
 	//
 	// The following will check the query part
 	if u.RawQuery != "" {
@@ -105,7 +105,7 @@ func (h UptimeKuma) Describe(callback func(service, params string)) {
 
 // UptimeKumaResponse is for parsing the response from Uptime Kuma.
 type UptimeKumaResponse struct {
-	Ok  bool   `json:"ok"`
+	OK  bool   `json:"ok"`
 	Msg string `json:"msg"`
 }
 
@@ -142,7 +142,7 @@ func (h UptimeKuma) ping(ctx context.Context, ppfmt pp.PP, param UptimeKumaReque
 		ppfmt.Warningf(pp.EmojiError, "Failed to parse the response from Uptime Kuma: %v", err)
 		return false
 	}
-	if !parsedResp.Ok {
+	if !parsedResp.OK {
 		ppfmt.Warningf(pp.EmojiError, "Failed to ping Uptime Kuma: %q", parsedResp.Msg)
 		return false
 	}
diff --git a/internal/notifier/shoutrrr.go b/internal/notifier/shoutrrr.go
index 89ef849..b0bc0fa 100644
--- a/internal/notifier/shoutrrr.go
+++ b/internal/notifier/shoutrrr.go
@@ -56,15 +56,15 @@ func (s Shoutrrr) Describe(callback func(service, params string)) {
 // Send sents the message msg.
 func (s Shoutrrr) Send(_ context.Context, ppfmt pp.PP, msg string) bool {
 	errs := s.Router.Send(msg, &types.Params{})
-	allOk := true
+	allOK := true
 	for _, err := range errs {
 		if err != nil {
-			ppfmt.Warningf(pp.EmojiError, "Failed to send shoutrrr message: %v", err)
-			allOk = false
+			ppfmt.Warningf(pp.EmojiError, "Failed to notify shoutrrr service(s): %v", err)
+			allOK = false
 		}
 	}
-	if allOk {
-		ppfmt.Infof(pp.EmojiMessage, "Sent shoutrrr message")
+	if allOK {
+		ppfmt.Infof(pp.EmojiNotify, "Notified %s via shoutrrr", pp.QuotedEnglishJoin(s.ServiceNames))
 	}
-	return allOk
+	return allOK
 }
diff --git a/internal/pp/emoji.go b/internal/pp/emoji.go
index 2d002f8..30f15b3 100644
--- a/internal/pp/emoji.go
+++ b/internal/pp/emoji.go
@@ -20,12 +20,12 @@ const (
 	EmojiUpdate   Emoji = "📡" // updating DNS records
 	EmojiClear    Emoji = "🧹" // clearing DNS records when exiting
 
-	EmojiPing    Emoji = "🔔" // pinging and health checks
-	EmojiMessage Emoji = "📨" // notifications
+	EmojiPing   Emoji = "🔔" // pinging and health checks
+	EmojiNotify Emoji = "📣" // notifications
 
+	EmojiTimeout     Emoji = "⌛" // Timeout or abortion
 	EmojiSignal      Emoji = "🚨" // catching signals
 	EmojiAlreadyDone Emoji = "🤷" // DNS records were already up to date
-	EmojiBailingOut  Emoji = "💨" // bailing out
 	EmojiNow         Emoji = "🏃" // an event that is happening now or immediately
 	EmojiAlarm       Emoji = "⏰" // an event that is scheduled to happen, but not immediately
 	EmojiBye         Emoji = "👋" // bye!
diff --git a/internal/pp/hint.go b/internal/pp/hint.go
index 2ec784d..e7ad9af 100644
--- a/internal/pp/hint.go
+++ b/internal/pp/hint.go
@@ -10,5 +10,6 @@ const (
 	HintIP6DetectionFails
 	HintDetectionTimeouts
 	HintUpdateTimeouts
+	HintCloudflareWAFPermissions
 	Hint1111Blockage
 )
diff --git a/internal/setter/base.go b/internal/setter/base.go
index 720a058..ded7db2 100644
--- a/internal/setter/base.go
+++ b/internal/setter/base.go
@@ -20,10 +20,12 @@ import (
 // Setter uses [api.Handle] to update DNS records.
 type Setter interface {
 	// SanityCheck determines whether one should continue trying.
+	// The first return value indicates whether it passes the test.
+	// The second return value indicates whether we are certain about the result.
 	SanityCheck(
 		ctx context.Context,
 		ppfmt pp.PP,
-	) bool
+	) (bool, bool)
 
 	// Set sets a particular domain to the given IP address.
 	Set(
diff --git a/internal/setter/setter.go b/internal/setter/setter.go
index a0965c4..68b7d85 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) {
 }
 
 // SanityCheck calls [api.Handle.SanityCheck].
-func (s setter) SanityCheck(ctx context.Context, ppfmt pp.PP) bool {
+func (s setter) SanityCheck(ctx context.Context, ppfmt pp.PP) (bool, bool) {
 	return s.Handle.SanityCheck(ctx, ppfmt)
 }
 
@@ -179,13 +179,15 @@ func (s setter) Delete(ctx context.Context, ppfmt pp.PP, domain domain.Domain, i
 		return ResponseNoop
 	}
 
-	allOk := true
+	allOK := true
 	for _, id := range unmatchedIDs {
 		if !s.Handle.DeleteRecord(ctx, ppfmt, domain, ipnet, id) {
-			allOk = false
+			allOK = false
 
 			if ctx.Err() != nil {
-				ppfmt.Infof(pp.EmojiBailingOut, "Operation aborted (%v); bailing out . . .", ctx.Err())
+				ppfmt.Infof(pp.EmojiTimeout,
+					"Deletion of %s records of %q aborted by timeout or signals; records might be inconsistent",
+					recordType, domainDescription)
 				return ResponseFailed
 			}
 			continue
@@ -193,7 +195,7 @@ func (s setter) Delete(ctx context.Context, ppfmt pp.PP, domain domain.Domain, i
 
 		ppfmt.Noticef(pp.EmojiDeletion, "Deleted a stale %s record of %q (ID: %s)", recordType, domainDescription, id)
 	}
-	if !allOk {
+	if !allOK {
 		ppfmt.Warningf(pp.EmojiError,
 			"Failed to properly delete %s records of %q; records might be inconsistent",
 			recordType, domainDescription)
diff --git a/internal/signal/signal.go b/internal/signal/signal.go
index 22d26db..5f320d2 100644
--- a/internal/signal/signal.go
+++ b/internal/signal/signal.go
@@ -39,8 +39,8 @@ func (h Handle) TearDown() {
 	signal.Stop(h.channel)
 }
 
-// SleepUntil waits for a period of time. It returns false if it is interrupted by signals in [Signals].
-func (h Handle) SleepUntil(ppfmt pp.PP, t time.Time) bool {
+// ReportSignalsUntil waits for a period of time. It returns true if it is interrupted by signals in [Signals].
+func (h Handle) ReportSignalsUntil(ppfmt pp.PP, t time.Time) bool {
 	timer := time.NewTimer(time.Until(t))
 	for {
 		select {
@@ -49,9 +49,9 @@ func (h Handle) SleepUntil(ppfmt pp.PP, t time.Time) bool {
 				<-timer.C
 			}
 			ppfmt.Noticef(pp.EmojiSignal, "Caught signal: %v", sig)
-			return false
-		case <-timer.C:
 			return true
+		case <-timer.C:
+			return false
 		}
 	}
 }
diff --git a/internal/updater/message.go b/internal/updater/message.go
index 4b9a612..0133ed6 100644
--- a/internal/updater/message.go
+++ b/internal/updater/message.go
@@ -27,7 +27,7 @@ func generateDetectMessage(ipNet ipnet.Type, ok bool) message.Message {
 	}
 
 	return message.Message{
-		Ok:               false,
+		OK:               false,
 		MonitorMessages:  []string{fmt.Sprintf("Failed to detect %s address", ipNet.Describe())},
 		NotifierMessages: []string{fmt.Sprintf("Failed to detect the %s address.", ipNet.Describe())},
 	}
@@ -38,7 +38,7 @@ func generateUpdateMessage(ipNet ipnet.Type, ip netip.Addr, s setterResponses) m
 	case len(s[setter.ResponseFailed]) > 0 &&
 		len(s[setter.ResponseUpdated]) > 0:
 		return message.Message{
-			Ok: false,
+			OK: false,
 			MonitorMessages: []string{fmt.Sprintf(
 				"Failed to set %s (%s): %s",
 				ipNet.RecordType(),
@@ -56,7 +56,7 @@ func generateUpdateMessage(ipNet ipnet.Type, ip netip.Addr, s setterResponses) m
 
 	case len(s[setter.ResponseFailed]) > 0:
 		return message.Message{
-			Ok: false,
+			OK: false,
 			MonitorMessages: []string{fmt.Sprintf(
 				"Failed to set %s (%s): %s",
 				ipNet.RecordType(),
@@ -73,7 +73,7 @@ func generateUpdateMessage(ipNet ipnet.Type, ip netip.Addr, s setterResponses) m
 
 	case len(s[setter.ResponseUpdated]) > 0:
 		return message.Message{
-			Ok: true,
+			OK: true,
 			MonitorMessages: []string{fmt.Sprintf(
 				"Set %s (%s): %s",
 				ipNet.RecordType(),
@@ -89,7 +89,7 @@ func generateUpdateMessage(ipNet ipnet.Type, ip netip.Addr, s setterResponses) m
 		}
 
 	default:
-		return message.Message{Ok: true, MonitorMessages: []string{}, NotifierMessages: []string{}}
+		return message.Message{OK: true, MonitorMessages: []string{}, NotifierMessages: []string{}}
 	}
 }
 
@@ -98,7 +98,7 @@ func generateDeleteMessage(ipNet ipnet.Type, s setterResponses) message.Message
 	case len(s[setter.ResponseFailed]) > 0 &&
 		len(s[setter.ResponseUpdated]) > 0:
 		return message.Message{
-			Ok: false,
+			OK: false,
 			MonitorMessages: []string{fmt.Sprintf(
 				"Failed to delete %s: %s",
 				ipNet.RecordType(),
@@ -114,7 +114,7 @@ func generateDeleteMessage(ipNet ipnet.Type, s setterResponses) message.Message
 
 	case len(s[setter.ResponseFailed]) > 0:
 		return message.Message{
-			Ok: false,
+			OK: false,
 			MonitorMessages: []string{fmt.Sprintf(
 				"Failed to delete %s: %s",
 				ipNet.RecordType(),
@@ -129,7 +129,7 @@ func generateDeleteMessage(ipNet ipnet.Type, s setterResponses) message.Message
 
 	case len(s[setter.ResponseUpdated]) > 0:
 		return message.Message{
-			Ok: true,
+			OK: true,
 			MonitorMessages: []string{fmt.Sprintf(
 				"Deleted %s: %s",
 				ipNet.RecordType(),
@@ -143,6 +143,6 @@ func generateDeleteMessage(ipNet ipnet.Type, s setterResponses) message.Message
 		}
 
 	default:
-		return message.Message{Ok: true, MonitorMessages: []string{}, NotifierMessages: []string{}}
+		return message.Message{OK: true, MonitorMessages: []string{}, NotifierMessages: []string{}}
 	}
 }
diff --git a/internal/updater/message_waf.go b/internal/updater/message_waf.go
index 1c4dc94..90511d2 100644
--- a/internal/updater/message_waf.go
+++ b/internal/updater/message_waf.go
@@ -24,7 +24,7 @@ func generateUpdateWAFListMessage(s setterWAFListResponses) message.Message {
 	case len(s[setter.ResponseFailed]) > 0 &&
 		len(s[setter.ResponseUpdated]) > 0:
 		return message.Message{
-			Ok: false,
+			OK: false,
 			MonitorMessages: []string{
 				"Failed to set list(s): " + pp.Join(s[setter.ResponseFailed]),
 			},
@@ -37,7 +37,7 @@ func generateUpdateWAFListMessage(s setterWAFListResponses) message.Message {
 
 	case len(s[setter.ResponseFailed]) > 0:
 		return message.Message{
-			Ok: false,
+			OK: false,
 			MonitorMessages: []string{
 				"Failed to set list(s): " + pp.Join(s[setter.ResponseFailed]),
 			},
@@ -49,7 +49,7 @@ func generateUpdateWAFListMessage(s setterWAFListResponses) message.Message {
 
 	case len(s[setter.ResponseUpdated]) > 0:
 		return message.Message{
-			Ok: true,
+			OK: true,
 			MonitorMessages: []string{
 				"Set list(s): " + pp.Join(s[setter.ResponseUpdated]),
 			},
@@ -60,7 +60,7 @@ func generateUpdateWAFListMessage(s setterWAFListResponses) message.Message {
 		}
 
 	default:
-		return message.Message{Ok: true, MonitorMessages: []string{}, NotifierMessages: []string{}}
+		return message.Message{OK: true, MonitorMessages: []string{}, NotifierMessages: []string{}}
 	}
 }
 
@@ -70,7 +70,7 @@ func generateDeleteWAFListMessage(s setterWAFListResponses) message.Message {
 	case len(s[setter.ResponseFailed]) > 0 &&
 		len(s[setter.ResponseUpdated]) > 0:
 		return message.Message{
-			Ok: false,
+			OK: false,
 			MonitorMessages: []string{
 				"Failed to delete list(s): " + pp.Join(s[setter.ResponseFailed]),
 			},
@@ -83,7 +83,7 @@ func generateDeleteWAFListMessage(s setterWAFListResponses) message.Message {
 
 	case len(s[setter.ResponseFailed]) > 0:
 		return message.Message{
-			Ok: false,
+			OK: false,
 			MonitorMessages: []string{
 				"Failed to delete list(s): " + pp.Join(s[setter.ResponseFailed]),
 			},
@@ -95,7 +95,7 @@ func generateDeleteWAFListMessage(s setterWAFListResponses) message.Message {
 
 	case len(s[setter.ResponseUpdated]) > 0:
 		return message.Message{
-			Ok: true,
+			OK: true,
 			MonitorMessages: []string{
 				"Deleted list(s): " + pp.Join(s[setter.ResponseUpdated]),
 			},
@@ -106,6 +106,6 @@ func generateDeleteWAFListMessage(s setterWAFListResponses) message.Message {
 		}
 
 	default:
-		return message.Message{Ok: true, MonitorMessages: []string{}, NotifierMessages: []string{}}
+		return message.Message{OK: true, MonitorMessages: []string{}, NotifierMessages: []string{}}
 	}
 }
diff --git a/internal/updater/updater.go b/internal/updater/updater.go
index 43a8847..bf56fa9 100644
--- a/internal/updater/updater.go
+++ b/internal/updater/updater.go
@@ -171,7 +171,7 @@ func UpdateIPs(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Sett
 
 			// Note: If we can't detect the new IP address,
 			// it's probably better to leave existing records alone.
-			if msg.Ok {
+			if msg.OK {
 				detectedAnyIP = true
 				msgs = append(msgs, setIP(ctx, ppfmt, c, s, ipNet, ip))
 			}


Test patch:
diff --git a/internal/api/cloudflare_test.go b/internal/api/cloudflare_test.go
index 9aac5ab..21015c6 100644
--- a/internal/api/cloudflare_test.go
+++ b/internal/api/cloudflare_test.go
@@ -133,18 +133,7 @@ func handleVerifyToken(t *testing.T, w http.ResponseWriter, r *http.Request, res
 }
 
 func mockVerifyToken() string {
-	return fmt.Sprintf(`{
-  "result": { "id": "%s", "status": "active" },
-  "success": true,
-  "errors": [],
-  "messages": [
-    {
-      "code": 10000,
-      "message": "This API Token is valid and active",
-      "type": null
-    }
-  ]
-}`, mockToken)
+	return fmt.Sprintf(`{"result":{"id":%q,"status":"active"},"success":true,"errors":[],"messages":[{"code":10000,"message":"This API Token is valid and active","type":null}]}`, mockToken) //nolint:lll
 }
 
 func newHandle(t *testing.T, ppfmt pp.PP, accountID string, httpStatus int, httpResponse string,
@@ -157,7 +146,7 @@ func newHandle(t *testing.T, ppfmt pp.PP, accountID string, httpStatus int, http
 		handleVerifyToken(t, w, r, httpStatus, httpResponse)
 	})
 
-	h, ok := auth.New(context.Background(), ppfmt, 8760*time.Hour) // a year
+	h, ok := auth.New(ppfmt, 8760*time.Hour) // a year
 	return mux, h, ok
 }
 
@@ -175,10 +164,14 @@ func TestNewValid(t *testing.T) {
 	_, h, ok := newGoodHandle(t, mockPP)
 	require.True(t, ok)
 
-	require.True(t, h.SanityCheck(context.Background(), mockPP))
+	ok, certain := h.SanityCheck(context.Background(), mockPP)
+	require.True(t, ok)
+	require.True(t, certain)
 
 	// Test again to test the caching
-	require.True(t, h.SanityCheck(context.Background(), mockPP))
+	ok, certain = h.SanityCheck(context.Background(), mockPP)
+	require.True(t, ok)
+	require.True(t, certain)
 }
 
 func TestNewEmptyToken(t *testing.T) {
@@ -190,161 +183,95 @@ func TestNewEmptyToken(t *testing.T) {
 
 	auth.Token = ""
 	mockPP.EXPECT().Errorf(pp.EmojiUserError, "Failed to prepare the Cloudflare authentication: %v", gomock.Any())
-	h, ok := auth.New(context.Background(), mockPP, time.Second)
+	h, ok := auth.New(mockPP, time.Second)
 	require.False(t, ok)
 	require.Nil(t, h)
 }
 
-func TestSanityCheckExpiring(t *testing.T) {
+func TestSanityCheck(t *testing.T) {
 	t.Parallel()
 
 	for name, tc := range map[string]struct {
-		httpStatus              int
-		httpResponse            string
 		okNew                   bool
 		prepareMocksNew         func(*mocks.MockPP)
+		httpStatus              int
+		httpResponse            string
+		certainSanityCheck      bool
 		okSanityCheck           bool
 		prepareMocksSanityCheck func(*mocks.MockPP)
 	}{
 		"expiring": {
-			http.StatusOK,
-			`{
-  "success": true,
-  "errors": [],
-  "messages": [],
-  "result": {
-    "id": "11111111111111111111111111111111",
-    "status": "active",
-    "expires_on": "3000-01-01T00:00:00Z"
-  }
-}`,
 			true,
 			nil,
-			true,
+			http.StatusOK,
+			`{"success":true,"errors":[],"messages":[],"result":{"id":"11111111111111111111111111111111","status":"active","expires_on":"3000-01-01T00:00:00Z"}}`, //nolint:lll
+			true, true,
 			func(p *mocks.MockPP) {
-				deadline, err := time.Parse(time.RFC3339, "3000-01-01T00:00:00Z")
+				_, err := time.Parse(time.RFC3339, "3000-01-01T00:00:00Z")
 				require.NoError(t, err)
-				p.EXPECT().Warningf(pp.EmojiAlarm, "The token will expire at %s",
-					deadline.In(time.Local).Format(time.RFC1123Z))
+				p.EXPECT().Warningf(pp.EmojiAlarm, "The Cloudflare API token will expire at %s (%v left)",
+					gomock.Any(), gomock.Any())
 			},
 		},
 		"expired": {
-			http.StatusOK,
-			`{
-  "success": true,
-  "errors": [],
-  "messages": [],
-  "result": {
-    "id": "11111111111111111111111111111111",
-    "status": "expired"
-  }
-}`,
 			true,
 			nil,
-			false,
+			http.StatusOK,
+			`{"success":true,"errors":[],"messages":[],"result":{"id":"11111111111111111111111111111111","status":"expired"}}`,
+			true, false,
 			func(p *mocks.MockPP) {
-				gomock.InOrder(
-					p.EXPECT().Errorf(pp.EmojiUserError, "The Cloudflare API token is %s", "expired"),
-					p.EXPECT().Errorf(pp.EmojiUserError, "Please double-check the value of CF_API_TOKEN or CF_API_TOKEN_FILE"),
-				)
+				p.EXPECT().Errorf(pp.EmojiUserError, "The Cloudflare API token is %s", "expired")
 			},
 		},
 		"funny": {
-			http.StatusOK,
-			`{
-  "success": true,
-  "errors": [],
-  "messages": [],
-  "result": {
-    "id": "11111111111111111111111111111111",
-    "status": "funny"
-  }
-}`,
 			true,
 			nil,
-			true,
+			http.StatusOK,
+			`{"success":true,"errors":[],"messages":[],"result":{"id":"11111111111111111111111111111111","status":"funny"}}`,
+			false, true,
 			func(p *mocks.MockPP) {
-				gomock.InOrder(
-					p.EXPECT().Warningf(pp.EmojiImpossible,
-						"The Cloudflare API token is in an undocumented state %q; please report this at %s",
-						"funny", pp.IssueReportingURL),
-				)
+				p.EXPECT().Warningf(pp.EmojiImpossible,
+					"The Cloudflare API token is in an undocumented state %q; please report this at %s",
+					"funny", pp.IssueReportingURL)
 			},
 		},
 		"disabled": {
-			http.StatusOK,
-			`{
-  "success": true,
-  "errors": [],
-  "messages": [],
-  "result": {
-    "id": "11111111111111111111111111111111",
-    "status": "disabled"
-  }
-}`,
 			true,
 			nil,
-			false,
+			http.StatusOK,
+			`{"success":true,"errors":[],"messages":[],"result":{"id":"11111111111111111111111111111111","status":"disabled"}}`,
+			true, false,
 			func(p *mocks.MockPP) {
-				gomock.InOrder(
-					p.EXPECT().Errorf(pp.EmojiUserError, "The Cloudflare API token is %s", "disabled"),
-					p.EXPECT().Errorf(pp.EmojiUserError, "Please double-check the value of CF_API_TOKEN or CF_API_TOKEN_FILE"),
-				)
+				p.EXPECT().Errorf(pp.EmojiUserError, "The Cloudflare API token is %s", "disabled")
 			},
 		},
 		"invalid-token": {
-			http.StatusUnauthorized,
-			`{
-  "success": false,
-  "errors": [{ "code": 1000, "message": "Invalid API Token" }],
-  "messages": [],
-  "result": null
-}`,
 			true,
 			nil,
-			false,
+			http.StatusUnauthorized,
+			`{"success":false,"errors":[{"code":1000,"message":"Invalid API Token"}],"messages":[],"result":null}`,
+			true, false,
 			func(p *mocks.MockPP) {
-				gomock.InOrder(
-					p.EXPECT().Errorf(pp.EmojiUserError, "The Cloudflare API token is invalid: %v", gomock.Any()),
-					p.EXPECT().Errorf(pp.EmojiUserError, "Please double-check the value of CF_API_TOKEN or CF_API_TOKEN_FILE"),
-				)
+				p.EXPECT().Errorf(pp.EmojiUserError, "The Cloudflare API token is invalid; please check the value of CF_API_TOKEN or CF_API_TOKEN_FILE") //nolint:lll
 			},
 		},
 		"invalid-format": {
-			http.StatusUnauthorized,
-			`{
-  "success": false,
-  "errors": [
-    {
-      "code": 6003,
-      "message": "Invalid request headers",
-      "error_chain": [
-        { "code": 6111, "message": "Invalid format for Authorization header" }
-      ]
-    }
-  ],
-  "messages": [],
-  "result": null
-}`,
 			true,
 			nil,
-			false,
+			http.StatusBadRequest,
+			`{"success":false,"errors":[{"code":6003,"message": "Invalid request headers","error_chain":[{"code":6111,"message": "Invalid format for Authorization header" }]}],"messages":[],"result":null}`, //nolint:lll
+			true, false,
 			func(p *mocks.MockPP) {
-				gomock.InOrder(
-					p.EXPECT().Errorf(pp.EmojiUserError, "The Cloudflare API token is invalid: %v", gomock.Any()),
-					p.EXPECT().Errorf(pp.EmojiUserError, "Please double-check the value of CF_API_TOKEN or CF_API_TOKEN_FILE"),
-				)
+				p.EXPECT().Errorf(pp.EmojiUserError, "The Cloudflare API token is invalid; please check the value of CF_API_TOKEN or CF_API_TOKEN_FILE") //nolint:lll
 			},
 		},
 		"invalid-json": {
+			true,
+			nil,
 			http.StatusOK,
 			`{`,
-			true,
+			false, true,
 			nil,
-			true,
-			func(p *mocks.MockPP) {
-				p.EXPECT().Warningf(pp.EmojiWarning, "Failed to verify the Cloudflare API token; will retry later: %v", gomock.Any()) //nolint:lll
-			},
 		},
 	} {
 		t.Run(name, func(t *testing.T) {
@@ -364,7 +291,10 @@ func TestSanityCheckExpiring(t *testing.T) {
 			if tc.prepareMocksSanityCheck != nil {
 				tc.prepareMocksSanityCheck(mockPP)
 			}
-			require.Equal(t, tc.okSanityCheck, h.SanityCheck(context.Background(), mockPP))
+
+			ok, certain := h.SanityCheck(context.Background(), mockPP)
+			require.Equal(t, tc.okSanityCheck, ok)
+			require.Equal(t, tc.certainSanityCheck, certain)
 		})
 	}
 }
@@ -384,9 +314,10 @@ func TestSanityCheckTimeout(t *testing.T) {
 		panic(http.ErrAbortHandler)
 	})
 
-	h, ok := auth.New(context.Background(), mockPP, time.Second)
+	h, ok := auth.New(mockPP, time.Second)
 	require.True(t, ok)
 	require.NotNil(t, h)
-	ok = h.SanityCheck(context.Background(), mockPP)
+	ok, certain := h.SanityCheck(context.Background(), mockPP)
 	require.True(t, ok)
+	require.False(t, certain)
 }
diff --git a/internal/api/cloudflare_waf_test.go b/internal/api/cloudflare_waf_test.go
index 3dc0bcd..521c181 100644
--- a/internal/api/cloudflare_waf_test.go
+++ b/internal/api/cloudflare_waf_test.go
@@ -166,6 +166,34 @@ func TestListWAFLists(t *testing.T) {
 	}
 }
 
+func TestListWAFListsHint(t *testing.T) {
+	t.Parallel()
+
+	mockCtrl := gomock.NewController(t)
+	mockPP := mocks.NewMockPP(mockCtrl)
+
+	mux, h, ok := newGoodHandle(t, mockPP)
+	require.True(t, ok)
+
+	mux.HandleFunc(fmt.Sprintf("GET /accounts/%s/rules/lists", mockAccountID),
+		func(w http.ResponseWriter, _ *http.Request) {
+			w.WriteHeader(http.StatusForbidden)
+			w.Header().Set("Content-Type", "application/json")
+			_, err := w.Write([]byte(`{"success":false,"errors":[{"code":10000,"message":"Authentication error"}]}`))
+			assert.NoError(t, err)
+		})
+
+	mockPP = mocks.NewMockPP(mockCtrl)
+	gomock.InOrder(
+		mockPP.EXPECT().Warningf(pp.EmojiError, "Failed to list existing lists: %v", gomock.Any()),
+		mockPP.EXPECT().Hintf(pp.HintCloudflareWAFPermissions,
+			`Make sure you granted the "Edit" permission of "Account - Account Filter Lists"`),
+	)
+	lists, ok := h.(api.CloudflareHandle).ListWAFLists(context.Background(), mockPP)
+	require.False(t, ok)
+	require.Zero(t, lists)
+}
+
 func TestFindWAFList(t *testing.T) {
 	t.Parallel()
 
diff --git a/internal/config/config_read_test.go b/internal/config/config_read_test.go
index 1703e97..b098f78 100644
--- a/internal/config/config_read_test.go
+++ b/internal/config/config_read_test.go
@@ -1,7 +1,6 @@
 package config_test
 
 import (
-	"context"
 	"testing"
 	"time"
 
@@ -91,7 +90,7 @@ type fakeAuth struct {
 	supportsWAFLists bool
 }
 
-func (a fakeAuth) New(context.Context, pp.PP, time.Duration) (api.Handle, bool) {
+func (a fakeAuth) New(pp.PP, time.Duration) (api.Handle, bool) {
 	return nil, false
 }
 func (a fakeAuth) SupportsRecords() bool  { return a.supportsRecords }
diff --git a/internal/file/file_test.go b/internal/file/file_test.go
index 6313ae3..114542f 100644
--- a/internal/file/file_test.go
+++ b/internal/file/file_test.go
@@ -22,7 +22,7 @@ func useMemFS(t *testing.T, memfs fstest.MapFS) {
 }
 
 //nolint:paralleltest // changing global var file.FS
-func TestReadStringOkay(t *testing.T) {
+func TestReadString(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 
 	path := "test/file.txt"
@@ -77,7 +77,7 @@ func TestReadStringNoAccess(t *testing.T) {
 }
 
 //nolint:paralleltest // reading global var file.FS
-func TestReadStringOkayAbsolutePath(t *testing.T) {
+func TestReadStringAbsolutePath(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 
 	path := "test/file.txt"
diff --git a/internal/monitor/composite_test.go b/internal/monitor/composite_test.go
index d8a7703..afe482a 100644
--- a/internal/monitor/composite_test.go
+++ b/internal/monitor/composite_test.go
@@ -164,7 +164,7 @@ func TestPingMessageAll(t *testing.T) {
 				}
 
 				msg := message.Message{
-					Ok:               tc2.ok,
+					OK:               tc2.ok,
 					MonitorMessages:  tc1.monitorMessages,
 					NotifierMessages: notifierMessages,
 				}
@@ -215,7 +215,7 @@ func TestLogMessageAll(t *testing.T) {
 				}
 
 				msg := message.Message{
-					Ok:               tc2.ok,
+					OK:               tc2.ok,
 					MonitorMessages:  tc1.monitorMessages,
 					NotifierMessages: notifierMessages,
 				}
diff --git a/internal/monitor/healthchecks_test.go b/internal/monitor/healthchecks_test.go
index 259b04c..a6d7fe9 100644
--- a/internal/monitor/healthchecks_test.go
+++ b/internal/monitor/healthchecks_test.go
@@ -88,8 +88,8 @@ func TestHealthchecksEndPoints(t *testing.T) {
 
 	type action int
 	const (
-		ActionOk action = iota
-		ActionNotOk
+		ActionOK action = iota
+		ActionNotOK
 		ActionAbort
 		ActionFail
 	)
@@ -109,7 +109,7 @@ func TestHealthchecksEndPoints(t *testing.T) {
 				return m.Success(context.Background(), ppfmt, "hello")
 			},
 			"/", "hello",
-			[]action{ActionAbort, ActionAbort, ActionOk},
+			[]action{ActionAbort, ActionAbort, ActionOK},
 			ActionAbort,
 			true, true,
 			func(m *mocks.MockPP) {
@@ -124,7 +124,7 @@ func TestHealthchecksEndPoints(t *testing.T) {
 				return m.Success(context.Background(), ppfmt, "aloha")
 			},
 			"/", "aloha",
-			[]action{ActionAbort, ActionAbort, ActionNotOk},
+			[]action{ActionAbort, ActionAbort, ActionNotOK},
 			ActionAbort,
 			false, false,
 			func(m *mocks.MockPP) {
@@ -154,7 +154,7 @@ func TestHealthchecksEndPoints(t *testing.T) {
 				return m.Start(context.Background(), ppfmt, "starting now!")
 			},
 			"/start", "starting now!",
-			[]action{ActionAbort, ActionAbort, ActionOk},
+			[]action{ActionAbort, ActionAbort, ActionOK},
 			ActionAbort,
 			true, true,
 			func(m *mocks.MockPP) {
@@ -169,7 +169,7 @@ func TestHealthchecksEndPoints(t *testing.T) {
 				return m.Failure(context.Background(), ppfmt, "something's wrong")
 			},
 			"/fail", "something's wrong",
-			[]action{ActionAbort, ActionAbort, ActionOk},
+			[]action{ActionAbort, ActionAbort, ActionOK},
 			ActionAbort,
 			true, true,
 			func(m *mocks.MockPP) {
@@ -184,7 +184,7 @@ func TestHealthchecksEndPoints(t *testing.T) {
 				return m.Log(context.Background(), ppfmt, "message")
 			},
 			"/log", "message",
-			[]action{ActionAbort, ActionAbort, ActionOk},
+			[]action{ActionAbort, ActionAbort, ActionOK},
 			ActionAbort,
 			true, true,
 			func(m *mocks.MockPP) {
@@ -199,7 +199,7 @@ func TestHealthchecksEndPoints(t *testing.T) {
 				return m.ExitStatus(context.Background(), ppfmt, 0, "bye!")
 			},
 			"/0", "bye!",
-			[]action{ActionAbort, ActionAbort, ActionOk},
+			[]action{ActionAbort, ActionAbort, ActionOK},
 			ActionAbort,
 			true, true,
 			func(m *mocks.MockPP) {
@@ -214,7 +214,7 @@ func TestHealthchecksEndPoints(t *testing.T) {
 				return m.ExitStatus(context.Background(), ppfmt, 1, "did exit(1)")
 			},
 			"/1", "did exit(1)",
-			[]action{ActionAbort, ActionAbort, ActionOk},
+			[]action{ActionAbort, ActionAbort, ActionOK},
 			ActionAbort,
 			true, true,
 			func(m *mocks.MockPP) {
@@ -266,12 +266,12 @@ func TestHealthchecksEndPoints(t *testing.T) {
 					action = tc.actions[visited-1]
 				}
 				switch action {
-				case ActionOk:
+				case ActionOK:
 					pinged = true
 					if _, err := io.WriteString(w, "OK"); !assert.NoError(t, err) {
 						panic(http.ErrAbortHandler)
 					}
-				case ActionNotOk:
+				case ActionNotOK:
 					w.WriteHeader(http.StatusBadRequest)
 					if _, err := io.WriteString(w, "invalid url format"); !assert.NoError(t, err) {
 						panic(http.ErrAbortHandler)
diff --git a/internal/monitor/uptimekuma_test.go b/internal/monitor/uptimekuma_test.go
index cde773a..6117b02 100644
--- a/internal/monitor/uptimekuma_test.go
+++ b/internal/monitor/uptimekuma_test.go
@@ -92,8 +92,8 @@ func TestUptimeKumaEndPoints(t *testing.T) {
 
 	type action int
 	const (
-		ActionOk action = iota
-		ActionNotOk
+		ActionOK action = iota
+		ActionNotOK
 		ActionGarbage
 		ActionAbort
 		ActionFail
@@ -123,7 +123,7 @@ func TestUptimeKumaEndPoints(t *testing.T) {
 				return m.Success(context.Background(), ppfmt, "hello")
 			},
 			"/", "up", "OK", "",
-			[]action{ActionOk},
+			[]action{ActionOK},
 			ActionAbort, true,
 			true,
 			successPP,
@@ -133,7 +133,7 @@ func TestUptimeKumaEndPoints(t *testing.T) {
 				return m.Success(context.Background(), ppfmt, "aloha")
 			},
 			"/", "up", "OK", "",
-			[]action{ActionNotOk},
+			[]action{ActionNotOK},
 			ActionAbort, false,
 			false,
 			func(m *mocks.MockPP) {
@@ -189,7 +189,7 @@ func TestUptimeKumaEndPoints(t *testing.T) {
 				return m.Failure(context.Background(), ppfmt, "something's wrong")
 			},
 			"/", "down", "something's wrong", "",
-			[]action{ActionOk},
+			[]action{ActionOK},
 			ActionAbort, true,
 			true,
 			successPP,
@@ -199,7 +199,7 @@ func TestUptimeKumaEndPoints(t *testing.T) {
 				return m.Failure(context.Background(), ppfmt, "")
 			},
 			"/", "down", "Failing", "",
-			[]action{ActionOk},
+			[]action{ActionOK},
 			ActionAbort, true,
 			true,
 			successPP,
@@ -233,7 +233,7 @@ func TestUptimeKumaEndPoints(t *testing.T) {
 				return m.ExitStatus(context.Background(), ppfmt, 1, "did exit(1)")
 			},
 			"/", "down", "did exit(1)", "",
-			[]action{ActionOk},
+			[]action{ActionOK},
 			ActionAbort, true,
 			true,
 			successPP,
@@ -243,7 +243,7 @@ func TestUptimeKumaEndPoints(t *testing.T) {
 				return m.ExitStatus(context.Background(), ppfmt, -1, "feeling negative")
 			},
 			"/", "down", "feeling negative", "",
-			[]action{ActionOk},
+			[]action{ActionOK},
 			ActionAbort, true,
 			true,
 			successPP,
@@ -286,12 +286,12 @@ func TestUptimeKumaEndPoints(t *testing.T) {
 					action = tc.actions[visited-1]
 				}
 				switch action {
-				case ActionOk:
+				case ActionOK:
 					pinged = true
 					if _, err := io.WriteString(w, `{"ok":true}`); !assert.NoError(t, err) {
 						panic(http.ErrAbortHandler)
 					}
-				case ActionNotOk:
+				case ActionNotOK:
 					if _, err := io.WriteString(w, `{"ok":false,"msg":"bad"}`); !assert.NoError(t, err) {
 						panic(http.ErrAbortHandler)
 					}
diff --git a/internal/notifier/composite_test.go b/internal/notifier/composite_test.go
index 203b4f9..fa14e24 100644
--- a/internal/notifier/composite_test.go
+++ b/internal/notifier/composite_test.go
@@ -86,7 +86,7 @@ func TestSendMessageAll(t *testing.T) {
 				}
 
 				msg := message.Message{
-					Ok:               tc2.ok,
+					OK:               tc2.ok,
 					MonitorMessages:  monitorMessages,
 					NotifierMessages: tc1.notifierMessages,
 				}
diff --git a/internal/notifier/shoutrrr_test.go b/internal/notifier/shoutrrr_test.go
index 418465f..bd1e146 100644
--- a/internal/notifier/shoutrrr_test.go
+++ b/internal/notifier/shoutrrr_test.go
@@ -45,7 +45,7 @@ func TestShoutrrrSend(t *testing.T) {
 			"hello",
 			true, true,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Infof(pp.EmojiMessage, "Sent shoutrrr message")
+				m.EXPECT().Infof(pp.EmojiNotify, "Notified %s via shoutrrr", `"generic"`)
 			},
 		},
 		"ill-formed url": {
@@ -54,7 +54,7 @@ func TestShoutrrrSend(t *testing.T) {
 			"hello",
 			false, false,
 			func(m *mocks.MockPP) {
-				m.EXPECT().Warningf(pp.EmojiError, "Failed to send shoutrrr message: %v", gomock.Any())
+				m.EXPECT().Warningf(pp.EmojiError, "Failed to notify shoutrrr service(s): %v", gomock.Any())
 			},
 		},
 	} {
diff --git a/internal/setter/setter_test.go b/internal/setter/setter_test.go
index c56cce1..3fc9d99 100644
--- a/internal/setter/setter_test.go
+++ b/internal/setter/setter_test.go
@@ -28,10 +28,13 @@ func TestSanityCheck(t *testing.T) {
 	t.Parallel()
 
 	for name, tc := range map[string]struct {
-		answer bool
+		ok      bool
+		certain bool
 	}{
-		"true":  {true},
-		"false": {false},
+		"true-true":   {true, true},
+		"true-false":  {true, false},
+		"false-true":  {false, true},
+		"false-false": {false, false},
 	} {
 		t.Run(name, func(t *testing.T) {
 			t.Parallel()
@@ -46,8 +49,10 @@ func TestSanityCheck(t *testing.T) {
 			s, ok := setter.New(mockPP, mockHandle)
 			require.True(t, ok)
 
-			mockHandle.EXPECT().SanityCheck(ctx, mockPP).Return(tc.answer)
-			require.Equal(t, tc.answer, s.SanityCheck(ctx, mockPP))
+			mockHandle.EXPECT().SanityCheck(ctx, mockPP).Return(tc.ok, tc.certain)
+			ok, certain := s.SanityCheck(ctx, mockPP)
+			require.Equal(t, tc.ok, ok)
+			require.Equal(t, tc.certain, certain)
 		})
 	}
 }
@@ -372,7 +377,9 @@ func TestDelete(t *testing.T) {
 						Return([]api.Record{{ID: record1, IP: ip1}}, true, true),
 					h.EXPECT().DeleteRecord(ctx, p, domain, ipNetwork, record1).
 						Do(wrapCancelAsDelete(cancel)).Return(false),
-					p.EXPECT().Infof(pp.EmojiBailingOut, "Operation aborted (%v); bailing out . . .", gomock.Any()),
+					p.EXPECT().Infof(pp.EmojiTimeout,
+						"Deletion of %s records of %q aborted by timeout or signals; records might be inconsistent",
+						"AAAA", "sub.test.org"),
 				)
 			},
 		},
diff --git a/internal/signal/signal_test.go b/internal/signal/signal_test.go
index d455a47..c343566 100644
--- a/internal/signal/signal_test.go
+++ b/internal/signal/signal_test.go
@@ -16,7 +16,7 @@ import (
 )
 
 //nolint:paralleltest //signals are global
-func TestSleep(t *testing.T) {
+func TestReportSignalUntil(t *testing.T) {
 	for name, tc := range map[string]struct {
 		alarmDelay    time.Duration
 		signalDelay   time.Duration
@@ -24,15 +24,15 @@ func TestSleep(t *testing.T) {
 		expected      bool
 		prepareMockPP func(m *mocks.MockPP)
 	}{
-		"no-signal": {time.Second / 10, 0, 0, true, nil},
+		"no-signal": {time.Second / 10, 0, 0, false, nil},
 		"sigint": {
-			time.Second, time.Second / 10, syscall.SIGINT, false,
+			time.Second, time.Second / 10, syscall.SIGINT, true,
 			func(m *mocks.MockPP) {
 				m.EXPECT().Noticef(pp.EmojiSignal, "Caught signal: %v", syscall.SIGINT)
 			},
 		},
 		"sigterm": {
-			time.Second, time.Second / 10, syscall.SIGTERM, false,
+			time.Second, time.Second / 10, syscall.SIGTERM, true,
 			func(m *mocks.MockPP) {
 				m.EXPECT().Noticef(pp.EmojiSignal, "Caught signal: %v", syscall.SIGTERM)
 			},
@@ -58,7 +58,7 @@ func TestSleep(t *testing.T) {
 			sig := signal.Setup()
 			go signalSelf()
 			target := time.Now().Add(tc.alarmDelay)
-			res := sig.SleepUntil(mockPP, target)
+			res := sig.ReportSignalsUntil(mockPP, target)
 			<-done
 			sig.TearDown()
 
diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go
index b3ee4be..bbb9aaf 100644
--- a/internal/updater/updater_test.go
+++ b/internal/updater/updater_test.go
@@ -168,7 +168,7 @@ func TestUpdateIPsMultiple(t *testing.T) {
 
 			resp := updater.UpdateIPs(ctx, mockPP, conf, mockSetter)
 			require.Equal(t, message.Message{
-				Ok:               tc.ok,
+				OK:               tc.ok,
 				NotifierMessages: tc.notifierMessages,
 				MonitorMessages:  tc.monitorMessages,
 			}, resp)
@@ -259,7 +259,7 @@ func TestDeleteIPsMultiple(t *testing.T) {
 			}
 			resp := updater.DeleteIPs(ctx, mockPP, conf, mockSetter)
 			require.Equal(t, message.Message{
-				Ok:               tc.ok,
+				OK:               tc.ok,
 				NotifierMessages: tc.notifierMessages,
 				MonitorMessages:  tc.monitorMessages,
 			}, resp)
@@ -327,7 +327,7 @@ func TestUpdateIPsUninitializedProxied(t *testing.T) {
 
 			resp := updater.UpdateIPs(ctx, mockPP, conf, mockSetter)
 			require.Equal(t, message.Message{
-				Ok:               tc.ok,
+				OK:               tc.ok,
 				NotifierMessages: tc.notifierMessages,
 				MonitorMessages:  tc.monitorMessages,
 			}, resp)
@@ -664,7 +664,7 @@ func TestUpdateIPs(t *testing.T) {
 			}
 			resp := updater.UpdateIPs(ctx, mockPP, conf, mockSetter)
 			require.Equal(t, message.Message{
-				Ok:               tc.ok,
+				OK:               tc.ok,
 				NotifierMessages: tc.notifierMessages,
 				MonitorMessages:  tc.monitorMessages,
 			}, resp)
@@ -871,7 +871,7 @@ func TestDeleteIPs(t *testing.T) {
 
 			resp := updater.DeleteIPs(ctx, mockPP, conf, mockSetter)
 			require.Equal(t, message.Message{
-				Ok:               tc.ok,
+				OK:               tc.ok,
 				NotifierMessages: tc.notifierMessages,
 				MonitorMessages:  tc.monitorMessages,
 			}, resp)