From c09e2b2ed965b9028a37d21d6a318fca48f539ca Mon Sep 17 00:00:00 2001 From: favonia Date: Mon, 24 Jun 2024 19:04:38 -0700 Subject: [PATCH] refactor(updater): smart message generation (part 3 of shoutrrr support) (#762) --- cmd/ddns/ddns.go | 24 +- internal/monitor/base.go | 11 + internal/monitor/composite.go | 22 +- internal/monitor/composite_test.go | 54 ++- internal/response/response.go | 35 ++ internal/setter/base.go | 12 - internal/setter/code.go | 20 + internal/setter/setter.go | 20 +- internal/setter/setter_test.go | 48 +-- internal/updater/response.go | 158 ++++++++ internal/updater/response_test.go | 45 +++ internal/updater/updater.go | 98 ++--- internal/updater/updater_test.go | 623 +++++++++++++++++++++-------- 13 files changed, 870 insertions(+), 300 deletions(-) create mode 100644 internal/response/response.go create mode 100644 internal/setter/code.go create mode 100644 internal/updater/response.go create mode 100644 internal/updater/response_test.go diff --git a/cmd/ddns/ddns.go b/cmd/ddns/ddns.go index e7367399..f86d13d2 100644 --- a/cmd/ddns/ddns.go +++ b/cmd/ddns/ddns.go @@ -55,11 +55,8 @@ func initConfig(ctx context.Context, ppfmt pp.PP) (*config.Config, setter.Setter func stopUpdating(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter) { if c.DeleteOnStop { - if ok, msg := updater.DeleteIPs(ctx, ppfmt, c, s); ok { - monitor.LogAll(ctx, ppfmt, msg, c.Monitors) - } else { - monitor.FailureAll(ctx, ppfmt, msg, c.Monitors) - } + resp := updater.DeleteIPs(ctx, ppfmt, c, s) + monitor.SendAll(ctx, ppfmt, c.Monitors, resp) } } @@ -94,10 +91,10 @@ func realMain() int { //nolint:funlen // Read the config and get the handler and the setter c, s, configOk := initConfig(ctx, ppfmt) // Ping the monitor regardless of whether initConfig succeeded - monitor.StartAll(ctx, ppfmt, formatName(), c.Monitors) + monitor.StartAll(ctx, ppfmt, c.Monitors, formatName()) // Bail out now if initConfig failed if !configOk { - monitor.ExitStatusAll(ctx, ppfmt, 1, "Config errors", c.Monitors) + monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 1, "Config errors") ppfmt.Infof(pp.EmojiBye, "Bye!") return 1 } @@ -121,13 +118,10 @@ func realMain() int { //nolint:funlen // Update the IP addresses if first && !c.UpdateOnStart { - monitor.SuccessAll(ctx, ppfmt, "Started (no action)", c.Monitors) + monitor.SuccessAll(ctx, ppfmt, c.Monitors, "Started (no action)") } else { - if ok, msg := updater.UpdateIPs(ctxWithSignals, ppfmt, c, s); ok { - monitor.SuccessAll(ctx, ppfmt, msg, c.Monitors) - } else { - monitor.FailureAll(ctx, ppfmt, msg, c.Monitors) - } + resp := updater.UpdateIPs(ctxWithSignals, ppfmt, c, s) + monitor.SendAll(ctx, ppfmt, c.Monitors, resp) } // Check if cron was disabled @@ -142,7 +136,7 @@ func realMain() int { //nolint:funlen if next.IsZero() { ppfmt.Errorf(pp.EmojiUserError, "No scheduled updates in near future") stopUpdating(ctx, ppfmt, c, s) - monitor.ExitStatusAll(ctx, ppfmt, 1, "No scheduled updates", c.Monitors) + monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 1, "No scheduled updates") ppfmt.Infof(pp.EmojiBye, "Bye!") return 1 } @@ -153,7 +147,7 @@ func realMain() int { //nolint:funlen // Wait for the next signal or the alarm, whichever comes first if !sig.SleepUntil(ppfmt, next) { stopUpdating(ctx, ppfmt, c, s) - monitor.ExitStatusAll(ctx, ppfmt, 0, "Terminated", c.Monitors) + monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 0, "Terminated") ppfmt.Infof(pp.EmojiBye, "Bye!") return 0 } diff --git a/internal/monitor/base.go b/internal/monitor/base.go index a16083b7..613e1432 100644 --- a/internal/monitor/base.go +++ b/internal/monitor/base.go @@ -3,8 +3,10 @@ package monitor import ( "context" + "strings" "github.com/favonia/cloudflare-ddns/internal/pp" + "github.com/favonia/cloudflare-ddns/internal/response" ) //go:generate mockgen -typed -destination=../mocks/mock_monitor.go -package=mocks . Monitor @@ -33,3 +35,12 @@ type Monitor interface { // ExitStatus records the exit status (as an integer in the POSIX style). ExitStatus(ctx context.Context, ppfmt pp.PP, code int, message string) bool } + +func Send(ctx context.Context, ppfmt pp.PP, m Monitor, r response.Response) bool { + msg := strings.Join(r.MonitorMessages, "\n") + if r.Ok { + return m.Log(ctx, ppfmt, msg) + } else { + return m.Failure(ctx, ppfmt, msg) + } +} diff --git a/internal/monitor/composite.go b/internal/monitor/composite.go index a6e5aac6..76ef2128 100644 --- a/internal/monitor/composite.go +++ b/internal/monitor/composite.go @@ -4,6 +4,7 @@ import ( "context" "github.com/favonia/cloudflare-ddns/internal/pp" + "github.com/favonia/cloudflare-ddns/internal/response" ) // DescribeAll calls [Monitor.Describe] for each monitor in the group with the callback. @@ -14,7 +15,7 @@ func DescribeAll(callback func(service, params string), ms []Monitor) { } // SuccessAll calls [Monitor.Success] for each monitor in the group. -func SuccessAll(ctx context.Context, ppfmt pp.PP, message string, ms []Monitor) bool { +func SuccessAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, message string) bool { ok := true for _, m := range ms { if !m.Success(ctx, ppfmt, message) { @@ -25,7 +26,7 @@ func SuccessAll(ctx context.Context, ppfmt pp.PP, message string, ms []Monitor) } // StartAll calls [Monitor.Start] for each monitor in ms. -func StartAll(ctx context.Context, ppfmt pp.PP, message string, ms []Monitor) bool { +func StartAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, message string) bool { ok := true for _, m := range ms { if !m.Start(ctx, ppfmt, message) { @@ -36,7 +37,7 @@ func StartAll(ctx context.Context, ppfmt pp.PP, message string, ms []Monitor) bo } // FailureAll calls [Monitor.Failure] for each monitor in ms. -func FailureAll(ctx context.Context, ppfmt pp.PP, message string, ms []Monitor) bool { +func FailureAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, message string) bool { ok := true for _, m := range ms { if !m.Failure(ctx, ppfmt, message) { @@ -47,7 +48,7 @@ func FailureAll(ctx context.Context, ppfmt pp.PP, message string, ms []Monitor) } // LogAll calls [Monitor.Log] for each monitor in ms. -func LogAll(ctx context.Context, ppfmt pp.PP, message string, ms []Monitor) bool { +func LogAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, message string) bool { ok := true for _, m := range ms { if !m.Log(ctx, ppfmt, message) { @@ -58,7 +59,7 @@ func LogAll(ctx context.Context, ppfmt pp.PP, message string, ms []Monitor) bool } // ExitStatusAll calls [Monitor.ExitStatus] for each monitor in ms. -func ExitStatusAll(ctx context.Context, ppfmt pp.PP, code int, message string, ms []Monitor) bool { +func ExitStatusAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, code int, message string) bool { ok := true for _, m := range ms { if !m.ExitStatus(ctx, ppfmt, code, message) { @@ -67,3 +68,14 @@ func ExitStatusAll(ctx context.Context, ppfmt pp.PP, code int, message string, m } return ok } + +// SendAll calls [Send] for each monitor in ms. +func SendAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, resp response.Response) bool { + ok := true + for _, m := range ms { + if !Send(ctx, ppfmt, m, resp) { + ok = false + } + } + return ok +} diff --git a/internal/monitor/composite_test.go b/internal/monitor/composite_test.go index 4f3dddae..50bd6906 100644 --- a/internal/monitor/composite_test.go +++ b/internal/monitor/composite_test.go @@ -2,12 +2,14 @@ package monitor_test import ( "context" + "strings" "testing" "go.uber.org/mock/gomock" "github.com/favonia/cloudflare-ddns/internal/mocks" "github.com/favonia/cloudflare-ddns/internal/monitor" + "github.com/favonia/cloudflare-ddns/internal/response" ) func TestDescribeAll(t *testing.T) { @@ -43,7 +45,7 @@ func TestSuccessAll(t *testing.T) { ms = append(ms, m) } - monitor.SuccessAll(context.Background(), mockPP, message, ms) + monitor.SuccessAll(context.Background(), mockPP, ms, message) } func TestStartAll(t *testing.T) { @@ -62,7 +64,7 @@ func TestStartAll(t *testing.T) { ms = append(ms, m) } - monitor.StartAll(context.Background(), mockPP, message, ms) + monitor.StartAll(context.Background(), mockPP, ms, message) } func TestFailureAll(t *testing.T) { @@ -81,7 +83,7 @@ func TestFailureAll(t *testing.T) { ms = append(ms, m) } - monitor.FailureAll(context.Background(), mockPP, message, ms) + monitor.FailureAll(context.Background(), mockPP, ms, message) } func TestLogAll(t *testing.T) { @@ -100,10 +102,10 @@ func TestLogAll(t *testing.T) { ms = append(ms, m) } - monitor.LogAll(context.Background(), mockPP, message, ms) + monitor.LogAll(context.Background(), mockPP, ms, message) } -func TestMonitorsExitStatus(t *testing.T) { +func TestExitStatusAll(t *testing.T) { t.Parallel() ms := make([]monitor.Monitor, 0, 5) @@ -119,5 +121,45 @@ func TestMonitorsExitStatus(t *testing.T) { ms = append(ms, m) } - monitor.ExitStatusAll(context.Background(), mockPP, 42, message, ms) + monitor.ExitStatusAll(context.Background(), mockPP, ms, 42, message) +} + +func TestSendAll(t *testing.T) { + t.Parallel() + + monitorMessages := []string{"forest", "grass"} + monitorMessage := strings.Join(monitorMessages, "\n") + notifierMessages := []string{"ocean"} + + for name, tc := range map[string]struct { + ok bool + }{ + "ok": {true}, + "notok": {false}, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + ms := make([]monitor.Monitor, 0, 5) + mockCtrl := gomock.NewController(t) + mockPP := mocks.NewMockPP(mockCtrl) + + for range 5 { + m := mocks.NewMockMonitor(mockCtrl) + if tc.ok { + m.EXPECT().Log(context.Background(), mockPP, monitorMessage) + } else { + m.EXPECT().Failure(context.Background(), mockPP, monitorMessage) + } + ms = append(ms, m) + } + + resp := response.Response{ + Ok: tc.ok, + MonitorMessages: monitorMessages, + NotifierMessages: notifierMessages, + } + monitor.SendAll(context.Background(), mockPP, ms, resp) + }) + } } diff --git a/internal/response/response.go b/internal/response/response.go new file mode 100644 index 00000000..e183d7d0 --- /dev/null +++ b/internal/response/response.go @@ -0,0 +1,35 @@ +package response + +type Response struct { + Ok bool + MonitorMessages []string + NotifierMessages []string +} + +func NewEmpty() Response { + return Response{ + Ok: true, + MonitorMessages: nil, + NotifierMessages: nil, + } +} + +func Merge(rs ...Response) Response { + var ( + allOk = true + allMonitorMessages = map[bool][]string{true: {}, false: {}} + allNotifierMessages = []string{} + ) + + for _, r := range rs { + allOk = allOk && r.Ok + allMonitorMessages[r.Ok] = append(allMonitorMessages[r.Ok], r.MonitorMessages...) + allNotifierMessages = append(allNotifierMessages, r.NotifierMessages...) + } + + return Response{ + Ok: allOk, + MonitorMessages: allMonitorMessages[allOk], + NotifierMessages: allNotifierMessages, + } +} diff --git a/internal/setter/base.go b/internal/setter/base.go index eb095350..0d3e4656 100644 --- a/internal/setter/base.go +++ b/internal/setter/base.go @@ -17,18 +17,6 @@ import ( //go:generate mockgen -typed -destination=../mocks/mock_setter.go -package=mocks . Setter -// ResponseCode encodes the minimum information to generate messages for monitors and notifiers. -type ResponseCode int - -const ( - // No updates were needed. The records were already okay. - ResponseNoUpdatesNeeded = iota - // Updates were needed and they are done. - ResponseUpdatesApplied - // Updates were needed and they did not fully complete. The records may be inconsistent. - ResponseUpdatesFailed -) - // Setter uses [api.Handle] to update DNS records. type Setter interface { // Set sets a particular domain to the given IP address. diff --git a/internal/setter/code.go b/internal/setter/code.go new file mode 100644 index 00000000..136479fb --- /dev/null +++ b/internal/setter/code.go @@ -0,0 +1,20 @@ +package setter + +// ResponseCode encodes the minimum information to generate messages for monitors and notifiers. +type ResponseCode int + +const ( + // ResponseNoop means no changes were needed. + // The records were already updated or already deleted. + ResponseNoop ResponseCode = iota + + // ResponseUpdated means records should be updated + // and we updated them, or that they should be deleted + // and we deleted them. + ResponseUpdated + + // ResponseFailed means records should be updated + // but we failed to finish the updating, or that they + // should be deleted and we failed to finish the deletion. + ResponseFailed +) diff --git a/internal/setter/setter.go b/internal/setter/setter.go index ccac2d49..1453586f 100644 --- a/internal/setter/setter.go +++ b/internal/setter/setter.go @@ -55,7 +55,7 @@ func (s *setter) Set(ctx context.Context, ppfmt pp.PP, rs, ok := s.Handle.ListRecords(ctx, ppfmt, domain, ipnet) if !ok { ppfmt.Errorf(pp.EmojiError, "Failed to retrieve the current %s records of %q", recordType, domainDescription) - return ResponseUpdatesFailed + return ResponseFailed } // The intention of these two lists is to find or create a good record and then delete everything else. @@ -77,7 +77,7 @@ func (s *setter) Set(ctx context.Context, ppfmt pp.PP, // If it's up to date and there are no other records, we are done! if foundMatched && len(unprocessedMatched) == 0 && len(unprocessedUnmatched) == 0 { ppfmt.Infof(pp.EmojiAlreadyDone, "The %s records of %q are already up to date", recordType, domainDescription) - return ResponseNoUpdatesNeeded + return ResponseNoop } // This counts the stale records that have not being deleted yet. @@ -164,12 +164,12 @@ func (s *setter) Set(ctx context.Context, ppfmt pp.PP, // Check whether we are done. It is okay to have duplicates, but it is not okay to have remaining stale records. if !foundMatched || numUndeletedUnmatched > 0 { ppfmt.Errorf(pp.EmojiError, - "Failed to complete updating of %s records of %q; records might be inconsistent", + "Failed to finish updating %s records of %q; records might be inconsistent", recordType, domainDescription) - return ResponseUpdatesFailed + return ResponseFailed } - return ResponseUpdatesApplied + return ResponseUpdated } // Delete deletes all managed DNS records. @@ -180,7 +180,7 @@ func (s *setter) Delete(ctx context.Context, ppfmt pp.PP, domain domain.Domain, rmap, ok := s.Handle.ListRecords(ctx, ppfmt, domain, ipnet) if !ok { ppfmt.Errorf(pp.EmojiError, "Failed to retrieve the current %s records of %q", recordType, domainDescription) - return ResponseUpdatesFailed + return ResponseFailed } // Sorting is not needed for correctness, but it will make the function deterministic. @@ -192,7 +192,7 @@ func (s *setter) Delete(ctx context.Context, ppfmt pp.PP, domain domain.Domain, if len(unmatchedIDs) == 0 { ppfmt.Infof(pp.EmojiAlreadyDone, "The %s records of %q were already deleted", recordType, domainDescription) - return ResponseNoUpdatesNeeded + return ResponseNoop } allOk := true @@ -206,10 +206,10 @@ func (s *setter) Delete(ctx context.Context, ppfmt pp.PP, domain domain.Domain, } if !allOk { ppfmt.Errorf(pp.EmojiError, - "Failed to complete deleting of %s records of %q; records might be inconsistent", + "Failed to finish deleting %s records of %q; records might be inconsistent", recordType, domainDescription) - return ResponseUpdatesFailed + return ResponseFailed } - return ResponseUpdatesApplied + return ResponseUpdated } diff --git a/internal/setter/setter_test.go b/internal/setter/setter_test.go index 821aea53..4b5a3513 100644 --- a/internal/setter/setter_test.go +++ b/internal/setter/setter_test.go @@ -42,7 +42,7 @@ func TestSet(t *testing.T) { }{ "0/1-false": { ip1, - setter.ResponseUpdatesApplied, + setter.ResponseUpdated, 1, false, func(m *mocks.MockPP) { @@ -57,7 +57,7 @@ func TestSet(t *testing.T) { }, "1unmatched/300-false": { ip1, - setter.ResponseUpdatesApplied, + setter.ResponseUpdated, 300, false, func(m *mocks.MockPP) { @@ -77,7 +77,7 @@ func TestSet(t *testing.T) { }, "1unmatched-updatefail/300-false": { ip1, - setter.ResponseUpdatesApplied, + setter.ResponseUpdated, 300, false, func(m *mocks.MockPP) { @@ -97,7 +97,7 @@ func TestSet(t *testing.T) { }, "1matched/300-false": { ip1, - setter.ResponseNoUpdatesNeeded, + setter.ResponseNoop, 300, false, func(m *mocks.MockPP) { @@ -109,7 +109,7 @@ func TestSet(t *testing.T) { }, "2matched/300-false": { ip1, - setter.ResponseUpdatesApplied, + setter.ResponseUpdated, 300, false, func(m *mocks.MockPP) { @@ -130,7 +130,7 @@ func TestSet(t *testing.T) { }, "2matched-deletefail/300-false": { ip1, - setter.ResponseUpdatesApplied, + setter.ResponseUpdated, 300, false, nil, @@ -143,7 +143,7 @@ func TestSet(t *testing.T) { }, "2unmatched/300-false": { ip1, - setter.ResponseUpdatesApplied, + setter.ResponseUpdated, 300, false, func(m *mocks.MockPP) { @@ -172,7 +172,7 @@ func TestSet(t *testing.T) { }, "2unmatched-updatefail/300-false": { ip1, - setter.ResponseUpdatesApplied, + setter.ResponseUpdated, 300, false, func(m *mocks.MockPP) { @@ -198,7 +198,7 @@ func TestSet(t *testing.T) { }, "2unmatched-updatefailtwice/300-false": { ip1, - setter.ResponseUpdatesApplied, + setter.ResponseUpdated, 300, false, func(m *mocks.MockPP) { @@ -222,14 +222,14 @@ func TestSet(t *testing.T) { //nolint:dupl "2unmatched-updatefail-deletefail-updatefail/300-false": { ip1, - setter.ResponseUpdatesFailed, + setter.ResponseFailed, 300, false, func(m *mocks.MockPP) { gomock.InOrder( - m.EXPECT().Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %q)", "AAAA", "sub.test.org", record2), //nolint:lll - m.EXPECT().Noticef(pp.EmojiCreateRecord, "Added a new %s record of %q (ID: %q)", "AAAA", "sub.test.org", record3), //nolint:lll - m.EXPECT().Errorf(pp.EmojiError, "Failed to complete updating of %s records of %q; records might be inconsistent", "AAAA", "sub.test.org"), //nolint:lll + m.EXPECT().Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %q)", "AAAA", "sub.test.org", record2), //nolint:lll + m.EXPECT().Noticef(pp.EmojiCreateRecord, "Added a new %s record of %q (ID: %q)", "AAAA", "sub.test.org", record3), //nolint:lll + m.EXPECT().Errorf(pp.EmojiError, "Failed to finish updating %s records of %q; records might be inconsistent", "AAAA", "sub.test.org"), //nolint:lll ) }, func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) { @@ -246,14 +246,14 @@ func TestSet(t *testing.T) { //nolint:dupl "2unmatched-updatefailtwice-createfail/300-false": { ip1, - setter.ResponseUpdatesFailed, + setter.ResponseFailed, 300, false, func(m *mocks.MockPP) { gomock.InOrder( - m.EXPECT().Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %q)", "AAAA", "sub.test.org", record1), //nolint:lll - m.EXPECT().Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %q)", "AAAA", "sub.test.org", record2), //nolint:lll - m.EXPECT().Errorf(pp.EmojiError, "Failed to complete updating of %s records of %q; records might be inconsistent", "AAAA", "sub.test.org"), //nolint:lll + m.EXPECT().Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %q)", "AAAA", "sub.test.org", record1), //nolint:lll + m.EXPECT().Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %q)", "AAAA", "sub.test.org", record2), //nolint:lll + m.EXPECT().Errorf(pp.EmojiError, "Failed to finish updating %s records of %q; records might be inconsistent", "AAAA", "sub.test.org"), //nolint:lll ) }, func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) { @@ -269,7 +269,7 @@ func TestSet(t *testing.T) { }, "listfail/300-false": { ip1, - setter.ResponseUpdatesFailed, + setter.ResponseFailed, 300, false, func(m *mocks.MockPP) { @@ -326,7 +326,7 @@ func TestDelete(t *testing.T) { prepareMockHandle func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) }{ "0": { - setter.ResponseNoUpdatesNeeded, + setter.ResponseNoop, func(m *mocks.MockPP) { m.EXPECT().Infof(pp.EmojiAlreadyDone, "The %s records of %q were already deleted", "AAAA", "sub.test.org") }, @@ -335,7 +335,7 @@ func TestDelete(t *testing.T) { }, }, "1unmatched": { - setter.ResponseUpdatesApplied, + setter.ResponseUpdated, func(m *mocks.MockPP) { m.EXPECT().Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %q)", "AAAA", "sub.test.org", record1) //nolint:lll }, @@ -347,9 +347,9 @@ func TestDelete(t *testing.T) { }, }, "1unmatched/fail": { - setter.ResponseUpdatesFailed, + setter.ResponseFailed, func(m *mocks.MockPP) { - m.EXPECT().Errorf(pp.EmojiError, "Failed to complete deleting of %s records of %q; records might be inconsistent", "AAAA", "sub.test.org") //nolint:lll + m.EXPECT().Errorf(pp.EmojiError, "Failed to finish deleting %s records of %q; records might be inconsistent", "AAAA", "sub.test.org") //nolint:lll }, func(ctx context.Context, ppfmt pp.PP, m *mocks.MockHandle) { gomock.InOrder( @@ -359,7 +359,7 @@ func TestDelete(t *testing.T) { }, }, "impossible-records": { - setter.ResponseUpdatesApplied, + setter.ResponseUpdated, func(m *mocks.MockPP) { gomock.InOrder( m.EXPECT().Noticef(pp.EmojiDeleteRecord, "Deleted a stale %s record of %q (ID: %q)", "AAAA", "sub.test.org", record1), //nolint:lll @@ -375,7 +375,7 @@ func TestDelete(t *testing.T) { }, }, "listfail": { - setter.ResponseUpdatesFailed, + setter.ResponseFailed, func(m *mocks.MockPP) { m.EXPECT().Errorf(pp.EmojiError, "Failed to retrieve the current %s records of %q", "AAAA", "sub.test.org") }, diff --git a/internal/updater/response.go b/internal/updater/response.go new file mode 100644 index 00000000..8a4fad0c --- /dev/null +++ b/internal/updater/response.go @@ -0,0 +1,158 @@ +package updater + +import ( + "fmt" + "net/netip" + "strings" + + "github.com/favonia/cloudflare-ddns/internal/domain" + "github.com/favonia/cloudflare-ddns/internal/ipnet" + "github.com/favonia/cloudflare-ddns/internal/response" + "github.com/favonia/cloudflare-ddns/internal/setter" +) + +type SetterResponses map[setter.ResponseCode][]string + +func (s SetterResponses) Register(code setter.ResponseCode, d domain.Domain) { + s[code] = append(s[code], d.Describe()) +} + +func ListJoin(items []string) string { return strings.Join(items, ", ") } +func ListEnglishJoin(items []string) string { + switch l := len(items); l { + case 0: + return "(none)" + case 1: + return items[0] + case 2: //nolint:mnd + return fmt.Sprintf("%s and %s", items[0], items[1]) + default: + return fmt.Sprintf("%s, and %s", strings.Join(items[:l-1], ", "), items[l-1]) + } +} + +func GenerateDetectResponse(ipNet ipnet.Type, ok bool) response.Response { + if ok { + return response.NewEmpty() + } + + return response.Response{ + 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())}, + } +} + +func GenerateUpdateResponse(ipNet ipnet.Type, ip netip.Addr, s SetterResponses) response.Response { + switch { + case len(s[setter.ResponseFailed]) > 0 && + len(s[setter.ResponseUpdated]) > 0: + return response.Response{ + Ok: false, + MonitorMessages: []string{fmt.Sprintf( + "Failed to set %s (%s): %s", + ipNet.RecordType(), + ip.String(), + ListJoin(s[setter.ResponseFailed]), + )}, + NotifierMessages: []string{fmt.Sprintf( + "Failed to finish updating %s records of %s with %s; those of %s were updated.", + ipNet.RecordType(), + ListEnglishJoin(s[setter.ResponseFailed]), + ip.String(), + ListEnglishJoin(s[setter.ResponseUpdated]), + )}, + } + + case len(s[setter.ResponseFailed]) > 0: + return response.Response{ + Ok: false, + MonitorMessages: []string{fmt.Sprintf( + "Failed to set %s (%s): %s", + ipNet.RecordType(), + ip.String(), + ListJoin(s[setter.ResponseFailed]), + )}, + NotifierMessages: []string{fmt.Sprintf( + "Failed to finish updating %s records of %s with %s.", + ipNet.RecordType(), + ListEnglishJoin(s[setter.ResponseFailed]), + ip.String(), + )}, + } + + case len(s[setter.ResponseUpdated]) > 0: + return response.Response{ + Ok: true, + MonitorMessages: []string{fmt.Sprintf( + "Set %s (%s): %s", + ipNet.RecordType(), + ip.String(), + ListJoin(s[setter.ResponseUpdated]), + )}, + NotifierMessages: []string{fmt.Sprintf( + "Updated %s records of %s with %s.", + ipNet.RecordType(), + ListEnglishJoin(s[setter.ResponseUpdated]), + ip.String(), + )}, + } + + default: + return response.Response{Ok: true, MonitorMessages: []string{}, NotifierMessages: []string{}} + } +} + +func GenerateDeleteResponse(ipNet ipnet.Type, s SetterResponses) response.Response { + switch { + case len(s[setter.ResponseFailed]) > 0 && + len(s[setter.ResponseUpdated]) > 0: + return response.Response{ + Ok: false, + MonitorMessages: []string{fmt.Sprintf( + "Failed to delete %s: %s", + ipNet.RecordType(), + ListJoin(s[setter.ResponseFailed]), + )}, + NotifierMessages: []string{fmt.Sprintf( + "Failed to finish deleting %s records of %s; those of %s were deleted.", + ipNet.RecordType(), + ListEnglishJoin(s[setter.ResponseFailed]), + ListEnglishJoin(s[setter.ResponseUpdated]), + )}, + } + + case len(s[setter.ResponseFailed]) > 0: + return response.Response{ + Ok: false, + MonitorMessages: []string{fmt.Sprintf( + "Failed to delete %s: %s", + ipNet.RecordType(), + ListJoin(s[setter.ResponseFailed]), + )}, + NotifierMessages: []string{fmt.Sprintf( + "Failed to finish deleting %s records of %s.", + ipNet.RecordType(), + ListEnglishJoin(s[setter.ResponseFailed]), + )}, + } + + case len(s[setter.ResponseUpdated]) > 0: + return response.Response{ + Ok: true, + MonitorMessages: []string{fmt.Sprintf( + "Deleted %s: %s", + ipNet.RecordType(), + ListJoin(s[setter.ResponseUpdated]), + )}, + NotifierMessages: []string{fmt.Sprintf( + "Deleted %s records of %s.", + ipNet.RecordType(), + ListEnglishJoin(s[setter.ResponseUpdated]), + )}, + } + + default: + return response.Response{Ok: true, MonitorMessages: []string{}, NotifierMessages: []string{}} + } +} diff --git a/internal/updater/response_test.go b/internal/updater/response_test.go new file mode 100644 index 00000000..5b3e3dc6 --- /dev/null +++ b/internal/updater/response_test.go @@ -0,0 +1,45 @@ +package updater_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/favonia/cloudflare-ddns/internal/updater" +) + +func TestListJoin(t *testing.T) { + t.Parallel() + for name, tc := range map[string]struct { + input []string + output string + }{ + "none": {nil, ""}, + "one": {[]string{"hello"}, "hello"}, + "two": {[]string{"hello", "hey"}, "hello, hey"}, + "three": {[]string{"hello", "hey", "hi"}, "hello, hey, hi"}, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tc.output, updater.ListJoin(tc.input)) + }) + } +} + +func TestListEnglishJoin(t *testing.T) { + t.Parallel() + for name, tc := range map[string]struct { + input []string + output string + }{ + "none": {nil, "(none)"}, + "one": {[]string{"hello"}, "hello"}, + "two": {[]string{"hello", "hey"}, "hello and hey"}, + "three": {[]string{"hello", "hey", "hi"}, "hello, hey, and hi"}, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tc.output, updater.ListEnglishJoin(tc.input)) + }) + } +} diff --git a/internal/updater/updater.go b/internal/updater/updater.go index ec0f50d3..4398702c 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -5,14 +5,13 @@ package updater import ( "context" "errors" - "fmt" "net/netip" - "strings" "github.com/favonia/cloudflare-ddns/internal/config" "github.com/favonia/cloudflare-ddns/internal/domain" "github.com/favonia/cloudflare-ddns/internal/ipnet" "github.com/favonia/cloudflare-ddns/internal/pp" + "github.com/favonia/cloudflare-ddns/internal/response" "github.com/favonia/cloudflare-ddns/internal/setter" ) @@ -53,75 +52,56 @@ var errSettingTimeout = errors.New("setting timeout") // ip must be non-zero. func setIP(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter, ipNet ipnet.Type, ip netip.Addr, -) (bool, []string) { - allOk := true - - // [msgs[false]] collects all the error messages and [msgs[true]] collects all the success messages. - msgs := map[bool][]string{} +) response.Response { + resps := SetterResponses{} for _, domain := range c.Domains[ipNet] { ctx, cancel := context.WithTimeoutCause(ctx, c.UpdateTimeout, errSettingTimeout) defer cancel() resp := s.Set(ctx, ppfmt, domain, ipNet, ip, c.TTL, getProxied(ppfmt, c, domain)) - switch resp { - case setter.ResponseUpdatesApplied: - msgs[true] = append(msgs[true], fmt.Sprintf("Set %s %s to %s", domain.Describe(), ipNet.RecordType(), ip.String())) - case setter.ResponseUpdatesFailed: - allOk = false - msgs[false] = append(msgs[false], fmt.Sprintf("Failed to set %s %s", domain.Describe(), ipNet.RecordType())) + resps.Register(resp, domain) + if resp == setter.ResponseFailed { if ShouldDisplayHints["update-timeout"] && errors.Is(context.Cause(ctx), errSettingTimeout) { ppfmt.Infof(pp.EmojiHint, "If your network is working but with high latency, consider increasing the value of UPDATE_TIMEOUT", ) ShouldDisplayHints["update-timeout"] = false } - case setter.ResponseNoUpdatesNeeded: } } - return allOk, msgs[allOk] + return GenerateUpdateResponse(ipNet, ip, resps) } -// deleteIP extracts relevant settings from the configuration and calls [setter.Setter.Clear] with a deadline. -func deleteIP(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter, ipNet ipnet.Type) (bool, []string) { - allOk := true - - // [msgs[false]] collects all the error messages and [msgs[true]] collects all the success messages. - msgs := map[bool][]string{} +// deleteIP extracts relevant settings from the configuration and calls [setter.Setter.Delete] with a deadline. +func deleteIP( + ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter, ipNet ipnet.Type, +) response.Response { + resps := SetterResponses{} for _, domain := range c.Domains[ipNet] { ctx, cancel := context.WithTimeout(ctx, c.UpdateTimeout) defer cancel() resp := s.Delete(ctx, ppfmt, domain, ipNet) - switch resp { - case setter.ResponseUpdatesApplied: - msgs[true] = append(msgs[true], fmt.Sprintf("Deleted %s %s", domain.Describe(), ipNet.RecordType())) - case setter.ResponseUpdatesFailed: - allOk = false - msgs[false] = append(msgs[false], fmt.Sprintf("Failed to delete %s %s", domain.Describe(), ipNet.RecordType())) - case setter.ResponseNoUpdatesNeeded: - } + resps.Register(resp, domain) } - return allOk, msgs[allOk] + return GenerateDeleteResponse(ipNet, resps) } func detectIP(ctx context.Context, ppfmt pp.PP, c *config.Config, ipNet ipnet.Type, use1001 bool, -) (netip.Addr, bool, []string) { +) (netip.Addr, bool) { ctx, cancel := context.WithTimeout(ctx, c.DetectionTimeout) defer cancel() - var msgs []string ip, ok := c.Provider[ipNet].GetIP(ctx, ppfmt, ipNet, use1001) if ok { ppfmt.Infof(pp.EmojiInternet, "Detected the %s address: %v", ipNet.Describe(), ip) } else { - msg := fmt.Sprintf("Failed to detect the %s address", ipNet.Describe()) - msgs = append(msgs, msg) - ppfmt.Errorf(pp.EmojiError, "%s", msg) + ppfmt.Errorf(pp.EmojiError, "Failed to detect the %s address", ipNet.Describe()) if ShouldDisplayHints[getHintIDForDetection(ipNet)] { switch ipNet { case ipnet.IP6: @@ -134,55 +114,37 @@ func detectIP(ctx context.Context, ppfmt pp.PP, } } ShouldDisplayHints[getHintIDForDetection(ipNet)] = false - return ip, ok, msgs + return ip, ok } // UpdateIPs detect IP addresses and update DNS records of managed domains. -func UpdateIPs(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter) (bool, string) { - allOk := true - - // [msgs[false]] collects all the error messages and [msgs[true]] collects all the success messages. - msgs := map[bool][]string{} +func UpdateIPs(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter) response.Response { + var resps []response.Response for _, ipNet := range [...]ipnet.Type{ipnet.IP4, ipnet.IP6} { if c.Provider[ipNet] != nil { - ip, ok, msg := detectIP(ctx, ppfmt, c, ipNet, c.Use1001) - msgs[ok] = append(msgs[ok], msg...) - if !ok { - // We can't detect the new IP address. It's probably better to leave existing IP addresses alone. - allOk = false - continue - } + ip, ok := detectIP(ctx, ppfmt, c, ipNet, c.Use1001) + resps = append(resps, GenerateDetectResponse(ipNet, ok)) - ok, msg = setIP(ctx, ppfmt, c, s, ipNet, ip) - msgs[ok] = append(msgs[ok], msg...) - allOk = allOk && ok + // Note: If we can't detect the new IP address, + // it's probably better to leave existing records alone. + if ok { + resps = append(resps, setIP(ctx, ppfmt, c, s, ipNet, ip)) + } } } - - var allMsg string - if len(msgs[false]) != 0 { - allMsg = strings.Join(msgs[false], "\n") - } else { - allMsg = strings.Join(msgs[true], "\n") - } - return allOk, allMsg + return response.Merge(resps...) } // DeleteIPs removes all DNS records of managed domains. -func DeleteIPs(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter) (bool, string) { - allOk := true - - // [msgs[false]] collects all the error messages and [msgs[true]] collects all the success messages. - msgs := map[bool][]string{} +func DeleteIPs(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter) response.Response { + var resps []response.Response for _, ipNet := range [...]ipnet.Type{ipnet.IP4, ipnet.IP6} { if c.Provider[ipNet] != nil { - ok, msg := deleteIP(ctx, ppfmt, c, s, ipNet) - allOk = allOk && ok - msgs[ok] = append(msgs[ok], msg...) + resps = append(resps, deleteIP(ctx, ppfmt, c, s, ipNet)) } } - return allOk, strings.Join(msgs[allOk], "\n") + return response.Merge(resps...) } diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go index b6f96ffc..3b4ca9d1 100644 --- a/internal/updater/updater_test.go +++ b/internal/updater/updater_test.go @@ -15,15 +15,381 @@ import ( "github.com/favonia/cloudflare-ddns/internal/ipnet" "github.com/favonia/cloudflare-ddns/internal/mocks" "github.com/favonia/cloudflare-ddns/internal/pp" + "github.com/favonia/cloudflare-ddns/internal/response" "github.com/favonia/cloudflare-ddns/internal/setter" "github.com/favonia/cloudflare-ddns/internal/updater" ) -//nolint:gochecknoglobals -var allHints = map[string]bool{ - "detect-ip4-fail": true, - "detect-ip6-fail": true, - "update-timeout": true, +//nolint:funlen,paralleltest // updater.IPv6MessageDisplayed is a global variable +func TestUpdateIPsMultiple(t *testing.T) { + domain4_1 := domain.FQDN("ip4.hello1") + domain4_2 := domain.FQDN("ip4.hello2") + domain4_3 := domain.FQDN("ip4.hello3") + domain4_4 := domain.FQDN("ip4.hello4") + domains := map[ipnet.Type][]domain.Domain{ + ipnet.IP4: {domain4_1, domain4_2, domain4_3, domain4_4}, + } + + ip4 := netip.MustParseAddr("127.0.0.1") + + type mockproviders = map[ipnet.Type]func(ppfmt pp.PP, m *mocks.MockProvider) + provider4 := func(ppfmt pp.PP, m *mocks.MockProvider) { + m.EXPECT().GetIP(gomock.Any(), ppfmt, ipnet.IP4, true).Return(ip4, true) + } + + for name, tc := range map[string]struct { + ok bool + monitorMessages []string + notifierMessages []string + prepareMockPP func(m *mocks.MockPP) + prepareMockProvider mockproviders + prepareMockSetter func(ppfmt pp.PP, m *mocks.MockSetter) + }{ + "2yes1no": { //nolint:dupl + false, + []string{"Failed to set A (127.0.0.1): ip4.hello2"}, + []string{"Failed to finish updating A records of ip4.hello2 with 127.0.0.1; those of ip4.hello1 and ip4.hello4 were updated."}, //nolint:lll + func(m *mocks.MockPP) { + m.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4) + }, + mockproviders{ipnet.IP4: provider4}, + func(ppfmt pp.PP, m *mocks.MockSetter) { + gomock.InOrder( + m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello1"), ipnet.IP4, ip4, api.TTLAuto, false). + Return(setter.ResponseUpdated), + m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello2"), ipnet.IP4, ip4, api.TTLAuto, false). + Return(setter.ResponseFailed), + m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello3"), ipnet.IP4, ip4, api.TTLAuto, false). + Return(setter.ResponseNoop), + m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello4"), ipnet.IP4, ip4, api.TTLAuto, false). + Return(setter.ResponseUpdated), + ) + }, + }, + "3yes": { //nolint:dupl + true, + []string{"Set A (127.0.0.1): ip4.hello1, ip4.hello3, ip4.hello4"}, + []string{"Updated A records of ip4.hello1, ip4.hello3, and ip4.hello4 with 127.0.0.1."}, + func(m *mocks.MockPP) { + m.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4) + }, + mockproviders{ipnet.IP4: provider4}, + func(ppfmt pp.PP, m *mocks.MockSetter) { + gomock.InOrder( + m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello1"), ipnet.IP4, ip4, api.TTLAuto, false). + Return(setter.ResponseUpdated), + m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello2"), ipnet.IP4, ip4, api.TTLAuto, false). + Return(setter.ResponseNoop), + m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello3"), ipnet.IP4, ip4, api.TTLAuto, false). + Return(setter.ResponseUpdated), + m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello4"), ipnet.IP4, ip4, api.TTLAuto, false). + Return(setter.ResponseUpdated), + ) + }, + }, + } { + t.Run(name, func(t *testing.T) { + mockCtrl := gomock.NewController(t) + ctx := context.Background() + conf := config.Default() + conf.Domains = domains + conf.TTL = api.TTLAuto + conf.Proxied = map[domain.Domain]bool{ + domain4_1: false, + domain4_2: false, + domain4_3: false, + domain4_4: false, + } + conf.Use1001 = true + conf.UpdateTimeout = time.Second + mockPP := mocks.NewMockPP(mockCtrl) + if tc.prepareMockPP != nil { + tc.prepareMockPP(mockPP) + } + for k := range updater.ShouldDisplayHints { + updater.ShouldDisplayHints[k] = true + } + for _, ipnet := range [...]ipnet.Type{ipnet.IP4, ipnet.IP6} { + if tc.prepareMockProvider[ipnet] == nil { + conf.Provider[ipnet] = nil + continue + } + mockProvider := mocks.NewMockProvider(mockCtrl) + tc.prepareMockProvider[ipnet](mockPP, mockProvider) + conf.Provider[ipnet] = mockProvider + } + mockSetter := mocks.NewMockSetter(mockCtrl) + if tc.prepareMockSetter != nil { + tc.prepareMockSetter(mockPP, mockSetter) + } + resp := updater.UpdateIPs(ctx, mockPP, conf, mockSetter) + require.Equal(t, response.Response{ + Ok: tc.ok, + NotifierMessages: tc.notifierMessages, + MonitorMessages: tc.monitorMessages, + }, resp) + }) + } +} + +//nolint:funlen,paralleltest // updater.IPv6MessageDisplayed is a global variable +func TestDeleteIPsMultiple(t *testing.T) { + domain4_1 := domain.FQDN("ip4.hello1") + domain4_2 := domain.FQDN("ip4.hello2") + domain4_3 := domain.FQDN("ip4.hello3") + domain4_4 := domain.FQDN("ip4.hello4") + domains := map[ipnet.Type][]domain.Domain{ + ipnet.IP4: {domain4_1, domain4_2, domain4_3, domain4_4}, + } + + for name, tc := range map[string]struct { + ok bool + monitorMessages []string + notifierMessages []string + prepareMockPP func(m *mocks.MockPP) + prepareMockSetter func(ppfmt pp.PP, m *mocks.MockSetter) + }{ + "2yes1no": { + false, + []string{"Failed to delete A: ip4.hello2"}, + []string{"Failed to finish deleting A records of ip4.hello2; those of ip4.hello1 and ip4.hello4 were deleted."}, //nolint:lll + nil, + func(ppfmt pp.PP, m *mocks.MockSetter) { + gomock.InOrder( + m.EXPECT().Delete(gomock.Any(), ppfmt, domain.FQDN("ip4.hello1"), ipnet.IP4). + Return(setter.ResponseUpdated), + m.EXPECT().Delete(gomock.Any(), ppfmt, domain.FQDN("ip4.hello2"), ipnet.IP4). + Return(setter.ResponseFailed), + m.EXPECT().Delete(gomock.Any(), ppfmt, domain.FQDN("ip4.hello3"), ipnet.IP4). + Return(setter.ResponseNoop), + m.EXPECT().Delete(gomock.Any(), ppfmt, domain.FQDN("ip4.hello4"), ipnet.IP4). + Return(setter.ResponseUpdated), + ) + }, + }, + "3yes": { + true, + []string{"Deleted A: ip4.hello1, ip4.hello3, ip4.hello4"}, + []string{"Deleted A records of ip4.hello1, ip4.hello3, and ip4.hello4."}, + nil, + func(ppfmt pp.PP, m *mocks.MockSetter) { + gomock.InOrder( + m.EXPECT().Delete(gomock.Any(), ppfmt, domain.FQDN("ip4.hello1"), ipnet.IP4). + Return(setter.ResponseUpdated), + m.EXPECT().Delete(gomock.Any(), ppfmt, domain.FQDN("ip4.hello2"), ipnet.IP4). + Return(setter.ResponseNoop), + m.EXPECT().Delete(gomock.Any(), ppfmt, domain.FQDN("ip4.hello3"), ipnet.IP4). + Return(setter.ResponseUpdated), + m.EXPECT().Delete(gomock.Any(), ppfmt, domain.FQDN("ip4.hello4"), ipnet.IP4). + Return(setter.ResponseUpdated), + ) + }, + }, + } { + t.Run(name, func(t *testing.T) { + mockCtrl := gomock.NewController(t) + ctx := context.Background() + conf := config.Default() + conf.Domains = domains + conf.TTL = api.TTLAuto + conf.Proxied = map[domain.Domain]bool{ + domain4_1: false, + domain4_2: false, + domain4_3: false, + domain4_4: false, + } + conf.Use1001 = true + conf.UpdateTimeout = time.Second + mockPP := mocks.NewMockPP(mockCtrl) + if tc.prepareMockPP != nil { + tc.prepareMockPP(mockPP) + } + for k := range updater.ShouldDisplayHints { + updater.ShouldDisplayHints[k] = true + } + for _, ipnet := range [...]ipnet.Type{ipnet.IP4, ipnet.IP6} { + conf.Provider[ipnet] = mocks.NewMockProvider(mockCtrl) + } + mockSetter := mocks.NewMockSetter(mockCtrl) + if tc.prepareMockSetter != nil { + tc.prepareMockSetter(mockPP, mockSetter) + } + resp := updater.DeleteIPs(ctx, mockPP, conf, mockSetter) + require.Equal(t, response.Response{ + Ok: tc.ok, + NotifierMessages: tc.notifierMessages, + MonitorMessages: tc.monitorMessages, + }, resp) + }) + } +} + +//nolint:funlen,paralleltest // updater.IPv6MessageDisplayed is a global variable +func TestUpdateIPsUninitializedProbied(t *testing.T) { + domain4 := domain.FQDN("ip4.hello") + domains := map[ipnet.Type][]domain.Domain{ + ipnet.IP4: {domain4}, + } + + ip4 := netip.MustParseAddr("127.0.0.1") + + type mockproviders = map[ipnet.Type]func(ppfmt pp.PP, m *mocks.MockProvider) + provider4 := func(ppfmt pp.PP, m *mocks.MockProvider) { + m.EXPECT().GetIP(gomock.Any(), ppfmt, ipnet.IP4, true).Return(ip4, true) + } + + for name, tc := range map[string]struct { + ok bool + monitorMessages []string + notifierMessages []string + prepareMockPP func(m *mocks.MockPP) + prepareMockProvider mockproviders + prepareMockSetter func(ppfmt pp.PP, m *mocks.MockSetter) + }{ + "ip4only-proxied-nil": { + true, + []string{"Set A (127.0.0.1): ip4.hello"}, + []string{"Updated A records of ip4.hello with 127.0.0.1."}, + func(m *mocks.MockPP) { + gomock.InOrder( + m.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4), + m.EXPECT().Warningf(pp.EmojiImpossible, + "Proxied[%s] not initialized; this should not happen; please report the bug at https://github.com/favonia/cloudflare-ddns/issues/new", //nolint:lll + "ip4.hello", + ), + ) + }, + mockproviders{ipnet.IP4: provider4}, + func(ppfmt pp.PP, m *mocks.MockSetter) { + m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTLAuto, false). + Return(setter.ResponseUpdated) + }, + }, + } { + t.Run(name, func(t *testing.T) { + mockCtrl := gomock.NewController(t) + ctx := context.Background() + conf := config.Default() + conf.Domains = domains + conf.TTL = api.TTLAuto + conf.Proxied = map[domain.Domain]bool{} + conf.Use1001 = true + conf.UpdateTimeout = time.Second + mockPP := mocks.NewMockPP(mockCtrl) + if tc.prepareMockPP != nil { + tc.prepareMockPP(mockPP) + } + for k := range updater.ShouldDisplayHints { + updater.ShouldDisplayHints[k] = true + } + for _, ipnet := range [...]ipnet.Type{ipnet.IP4, ipnet.IP6} { + if tc.prepareMockProvider[ipnet] == nil { + conf.Provider[ipnet] = nil + continue + } + mockProvider := mocks.NewMockProvider(mockCtrl) + tc.prepareMockProvider[ipnet](mockPP, mockProvider) + conf.Provider[ipnet] = mockProvider + } + mockSetter := mocks.NewMockSetter(mockCtrl) + if tc.prepareMockSetter != nil { + tc.prepareMockSetter(mockPP, mockSetter) + } + resp := updater.UpdateIPs(ctx, mockPP, conf, mockSetter) + require.Equal(t, response.Response{ + Ok: tc.ok, + NotifierMessages: tc.notifierMessages, + MonitorMessages: tc.monitorMessages, + }, resp) + }) + } +} + +//nolint:funlen,paralleltest // updater.IPv6MessageDisplayed is a global variable +func TestUpdateIPsHints(t *testing.T) { + domain4 := domain.FQDN("ip4.hello") + domain6 := domain.FQDN("ip6.hello") + domains := map[ipnet.Type][]domain.Domain{ + ipnet.IP4: {domain4}, + ipnet.IP6: {domain6}, + } + + ip4 := netip.MustParseAddr("127.0.0.1") + + type mockproviders = map[ipnet.Type]func(ppfmt pp.PP, m *mocks.MockProvider) + provider4 := func(ppfmt pp.PP, m *mocks.MockProvider) { + m.EXPECT().GetIP(gomock.Any(), ppfmt, ipnet.IP4, true).Return(ip4, true) + } + + for name, tc := range map[string]struct { + ShouldDisplayHints map[string]bool + ok bool + monitorMessages []string + notifierMessages []string + prepareMockPP func(m *mocks.MockPP) + prepareMockProvider mockproviders + prepareMockSetter func(ppfmt pp.PP, m *mocks.MockSetter) + }{ + "ip6fails/again": { + map[string]bool{"detect-ip4-fail": true, "detect-ip6-fail": false, "update-timeout": true}, + false, + []string{"Failed to detect IPv6 address"}, + []string{"Failed to detect the IPv6 address."}, + func(m *mocks.MockPP) { + gomock.InOrder( + m.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4), + m.EXPECT().Errorf(pp.EmojiError, "Failed to detect the %s address", "IPv6"), + ) + }, + mockproviders{ + ipnet.IP4: provider4, + ipnet.IP6: func(ppfmt pp.PP, m *mocks.MockProvider) { + m.EXPECT().GetIP(gomock.Any(), ppfmt, ipnet.IP6, true).Return(netip.Addr{}, false) + }, + }, + func(ppfmt pp.PP, m *mocks.MockSetter) { + m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTLAuto, false). + Return(setter.ResponseNoop) + }, + }, + } { + t.Run(name, func(t *testing.T) { + mockCtrl := gomock.NewController(t) + ctx := context.Background() + conf := config.Default() + conf.Domains = domains + conf.TTL = api.TTLAuto + conf.Proxied = map[domain.Domain]bool{domain4: false, domain6: false} + conf.Use1001 = true + conf.UpdateTimeout = time.Second + mockPP := mocks.NewMockPP(mockCtrl) + if tc.prepareMockPP != nil { + tc.prepareMockPP(mockPP) + } + for k := range updater.ShouldDisplayHints { + updater.ShouldDisplayHints[k] = tc.ShouldDisplayHints[k] + } + for _, ipnet := range [...]ipnet.Type{ipnet.IP4, ipnet.IP6} { + if tc.prepareMockProvider[ipnet] == nil { + conf.Provider[ipnet] = nil + continue + } + mockProvider := mocks.NewMockProvider(mockCtrl) + tc.prepareMockProvider[ipnet](mockPP, mockProvider) + conf.Provider[ipnet] = mockProvider + } + mockSetter := mocks.NewMockSetter(mockCtrl) + if tc.prepareMockSetter != nil { + tc.prepareMockSetter(mockPP, mockSetter) + } + resp := updater.UpdateIPs(ctx, mockPP, conf, mockSetter) + require.Equal(t, response.Response{ + Ok: tc.ok, + NotifierMessages: tc.notifierMessages, + MonitorMessages: tc.monitorMessages, + }, resp) + }) + } } //nolint:funlen,paralleltest // updater.IPv6MessageDisplayed is a global variable @@ -55,126 +421,113 @@ func TestUpdateIPs(t *testing.T) { m.EXPECT().GetIP(gomock.Any(), ppfmt, ipnet.IP6, true).Return(ip6, true) } - type mockproxied = map[domain.Domain]bool - proxiedNone := mockproxied{domain4: false, domain6: false} - proxiedBoth := mockproxied{domain4: true, domain6: true} - for name, tc := range map[string]struct { - proxied mockproxied ok bool - msg string - ShouldDisplayHints map[string]bool + monitorMessages []string + notifierMessages []string prepareMockPP func(m *mocks.MockPP) prepareMockProvider mockproviders prepareMockSetter func(ppfmt pp.PP, m *mocks.MockSetter) }{ "none": { - proxiedBoth, true, ``, allHints, nil, mockproviders{}, nil, + true, []string{}, []string{}, nil, mockproviders{}, nil, }, "ip4only": { - proxiedNone, true, - "", - allHints, + []string{}, + []string{}, pp4only, mockproviders{ipnet.IP4: provider4}, func(ppfmt pp.PP, m *mocks.MockSetter) { m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTLAuto, false). - Return(setter.ResponseNoUpdatesNeeded) + Return(setter.ResponseNoop) }, }, "ip4only/setfail": { - proxiedBoth, false, - "Failed to set ip4.hello A", - allHints, + []string{"Failed to set A (127.0.0.1): ip4.hello"}, + []string{"Failed to finish updating A records of ip4.hello with 127.0.0.1."}, pp4only, mockproviders{ipnet.IP4: provider4}, func(ppfmt pp.PP, m *mocks.MockSetter) { - m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTLAuto, true). - Return(setter.ResponseUpdatesFailed) + m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTLAuto, false). + Return(setter.ResponseFailed) }, }, "ip6only": { - proxiedNone, true, - "Set ip6.hello AAAA to ::1", - allHints, + []string{"Set AAAA (::1): ip6.hello"}, + []string{"Updated AAAA records of ip6.hello with ::1."}, pp6only, mockproviders{ipnet.IP6: provider6}, func(ppfmt pp.PP, m *mocks.MockSetter) { m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTLAuto, false). - Return(setter.ResponseUpdatesApplied) + Return(setter.ResponseUpdated) }, }, "ip6only/setfail": { - proxiedBoth, false, - "Failed to set ip6.hello AAAA", - allHints, + []string{"Failed to set AAAA (::1): ip6.hello"}, + []string{"Failed to finish updating AAAA records of ip6.hello with ::1."}, pp6only, mockproviders{ipnet.IP6: provider6}, func(ppfmt pp.PP, m *mocks.MockSetter) { - m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTLAuto, true). - Return(setter.ResponseUpdatesFailed) + m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTLAuto, false). + Return(setter.ResponseFailed) }, }, "both": { - proxiedNone, true, - "", - allHints, + []string{}, + []string{}, ppBoth, mockproviders{ipnet.IP4: provider4, ipnet.IP6: provider6}, func(ppfmt pp.PP, m *mocks.MockSetter) { gomock.InOrder( m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTLAuto, false). - Return(setter.ResponseNoUpdatesNeeded), + Return(setter.ResponseNoop), m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTLAuto, false). - Return(setter.ResponseNoUpdatesNeeded), + Return(setter.ResponseNoop), ) }, }, "both/setfail1": { - proxiedBoth, false, - "Failed to set ip4.hello A", - allHints, + []string{"Failed to set A (127.0.0.1): ip4.hello"}, + []string{"Failed to finish updating A records of ip4.hello with 127.0.0.1."}, ppBoth, mockproviders{ipnet.IP4: provider4, ipnet.IP6: provider6}, func(ppfmt pp.PP, m *mocks.MockSetter) { gomock.InOrder( - m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTLAuto, true). - Return(setter.ResponseUpdatesFailed), - m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTLAuto, true). - Return(setter.ResponseNoUpdatesNeeded), + m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTLAuto, false). + Return(setter.ResponseFailed), + m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTLAuto, false). + Return(setter.ResponseNoop), ) }, }, "both/setfail2": { - proxiedNone, false, - "Failed to set ip6.hello AAAA", - allHints, + []string{"Failed to set AAAA (::1): ip6.hello"}, + []string{"Failed to finish updating AAAA records of ip6.hello with ::1."}, ppBoth, mockproviders{ipnet.IP4: provider4, ipnet.IP6: provider6}, func(ppfmt pp.PP, m *mocks.MockSetter) { gomock.InOrder( m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTLAuto, false). - Return(setter.ResponseNoUpdatesNeeded), + Return(setter.ResponseNoop), m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTLAuto, false). - Return(setter.ResponseUpdatesFailed), + Return(setter.ResponseFailed), ) }, }, "ip4fails": { - proxiedBoth, false, - "Failed to detect the IPv4 address", - allHints, + []string{"Failed to detect IPv4 address"}, + []string{"Failed to detect the IPv4 address."}, func(m *mocks.MockPP) { gomock.InOrder( - m.EXPECT().Errorf(pp.EmojiError, "%s", "Failed to detect the IPv4 address"), + m.EXPECT().Errorf(pp.EmojiError, "Failed to detect the %s address", "IPv4"), m.EXPECT().Infof(pp.EmojiHint, "If your network does not support IPv4, you can disable it with IP4_PROVIDER=none"), //nolint:lll m.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv6", ip6), ) @@ -186,19 +539,18 @@ func TestUpdateIPs(t *testing.T) { ipnet.IP6: provider6, }, func(ppfmt pp.PP, m *mocks.MockSetter) { - m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTLAuto, true). - Return(setter.ResponseNoUpdatesNeeded) + m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip6.hello"), ipnet.IP6, ip6, api.TTLAuto, false). + Return(setter.ResponseNoop) }, }, "ip6fails": { - proxiedNone, false, - "Failed to detect the IPv6 address", - allHints, + []string{"Failed to detect IPv6 address"}, + []string{"Failed to detect the IPv6 address."}, func(m *mocks.MockPP) { gomock.InOrder( m.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4), - m.EXPECT().Errorf(pp.EmojiError, "%s", "Failed to detect the IPv6 address"), + m.EXPECT().Errorf(pp.EmojiError, "Failed to detect the %s address", "IPv6"), m.EXPECT().Infof(pp.EmojiHint, "If you are using Docker or Kubernetes, IPv6 often requires additional setups"), //nolint:lll m.EXPECT().Infof(pp.EmojiHint, "Read more about IPv6 networks at https://github.com/favonia/cloudflare-ddns"), //nolint:lll m.EXPECT().Infof(pp.EmojiHint, "If your network does not support IPv6, you can disable it with IP6_PROVIDER=none"), //nolint:lll @@ -212,41 +564,18 @@ func TestUpdateIPs(t *testing.T) { }, func(ppfmt pp.PP, m *mocks.MockSetter) { m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTLAuto, false). - Return(setter.ResponseNoUpdatesNeeded) - }, - }, - "ip6fails/again": { - proxiedBoth, - false, - "Failed to detect the IPv6 address", - map[string]bool{"detect-ip4-fail": true, "detect-ip6-fail": false, "update-timeout": true}, - func(m *mocks.MockPP) { - gomock.InOrder( - m.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4), - m.EXPECT().Errorf(pp.EmojiError, "%s", "Failed to detect the IPv6 address"), - ) - }, - mockproviders{ - ipnet.IP4: provider4, - ipnet.IP6: func(ppfmt pp.PP, m *mocks.MockProvider) { - m.EXPECT().GetIP(gomock.Any(), ppfmt, ipnet.IP6, true).Return(netip.Addr{}, false) - }, - }, - func(ppfmt pp.PP, m *mocks.MockSetter) { - m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTLAuto, true). - Return(setter.ResponseNoUpdatesNeeded) + Return(setter.ResponseNoop) }, }, "bothfail": { - proxiedNone, false, - "Failed to detect the IPv4 address\nFailed to detect the IPv6 address", - allHints, + []string{"Failed to detect IPv4 address", "Failed to detect IPv6 address"}, + []string{"Failed to detect the IPv4 address.", "Failed to detect the IPv6 address."}, func(m *mocks.MockPP) { gomock.InOrder( - m.EXPECT().Errorf(pp.EmojiError, "%s", "Failed to detect the IPv4 address"), + m.EXPECT().Errorf(pp.EmojiError, "Failed to detect the %s address", "IPv4"), m.EXPECT().Infof(pp.EmojiHint, "If your network does not support IPv4, you can disable it with IP4_PROVIDER=none"), //nolint:lll - m.EXPECT().Errorf(pp.EmojiError, "%s", "Failed to detect the IPv6 address"), + m.EXPECT().Errorf(pp.EmojiError, "Failed to detect the %s address", "IPv6"), m.EXPECT().Infof(pp.EmojiHint, "If you are using Docker or Kubernetes, IPv6 often requires additional setups"), //nolint:lll m.EXPECT().Infof(pp.EmojiHint, "Read more about IPv6 networks at https://github.com/favonia/cloudflare-ddns"), //nolint:lll m.EXPECT().Infof(pp.EmojiHint, "If your network does not support IPv6, you can disable it with IP6_PROVIDER=none"), //nolint:lll @@ -262,31 +591,10 @@ func TestUpdateIPs(t *testing.T) { }, nil, }, - "ip4only-proxied-nil": { - mockproxied{}, - true, - "Set ip4.hello A to 127.0.0.1", - allHints, - func(m *mocks.MockPP) { - gomock.InOrder( - m.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4), - m.EXPECT().Warningf(pp.EmojiImpossible, - "Proxied[%s] not initialized; this should not happen; please report the bug at https://github.com/favonia/cloudflare-ddns/issues/new", //nolint:lll - "ip4.hello", - ), - ) - }, - mockproviders{ipnet.IP4: provider4}, - func(ppfmt pp.PP, m *mocks.MockSetter) { - m.EXPECT().Set(gomock.Any(), ppfmt, domain.FQDN("ip4.hello"), ipnet.IP4, ip4, api.TTLAuto, false). - Return(setter.ResponseUpdatesApplied) - }, - }, "slow-setting": { - proxiedNone, false, - "Failed to set ip4.hello A", - allHints, + []string{"Failed to set A (127.0.0.1): ip4.hello"}, + []string{"Failed to finish updating A records of ip4.hello with 127.0.0.1."}, func(m *mocks.MockPP) { gomock.InOrder( m.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4), @@ -303,7 +611,7 @@ func TestUpdateIPs(t *testing.T) { DoAndReturn( func(_ context.Context, _ pp.PP, _ domain.Domain, _ ipnet.Type, _ netip.Addr, _ api.TTL, _ bool) setter.ResponseCode { //nolint:lll time.Sleep(2 * time.Second) - return setter.ResponseUpdatesFailed + return setter.ResponseFailed }) }, }, @@ -314,7 +622,7 @@ func TestUpdateIPs(t *testing.T) { conf := config.Default() conf.Domains = domains conf.TTL = api.TTLAuto - conf.Proxied = tc.proxied + conf.Proxied = map[domain.Domain]bool{domain4: false, domain6: false} conf.Use1001 = true conf.UpdateTimeout = time.Second mockPP := mocks.NewMockPP(mockCtrl) @@ -322,7 +630,7 @@ func TestUpdateIPs(t *testing.T) { tc.prepareMockPP(mockPP) } for k := range updater.ShouldDisplayHints { - updater.ShouldDisplayHints[k] = tc.ShouldDisplayHints[k] + updater.ShouldDisplayHints[k] = true } for _, ipnet := range [...]ipnet.Type{ipnet.IP4, ipnet.IP6} { if tc.prepareMockProvider[ipnet] == nil { @@ -337,9 +645,12 @@ func TestUpdateIPs(t *testing.T) { if tc.prepareMockSetter != nil { tc.prepareMockSetter(mockPP, mockSetter) } - ok, msg := updater.UpdateIPs(ctx, mockPP, conf, mockSetter) - require.Equal(t, tc.ok, ok) - require.Equal(t, tc.msg, msg) + resp := updater.UpdateIPs(ctx, mockPP, conf, mockSetter) + require.Equal(t, response.Response{ + Ok: tc.ok, + NotifierMessages: tc.notifierMessages, + MonitorMessages: tc.monitorMessages, + }, resp) }) } } @@ -355,111 +666,99 @@ func TestDeleteIPs(t *testing.T) { type mockproviders = map[ipnet.Type]bool - type mockproxied = map[domain.Domain]bool - proxiedNone := mockproxied{domain4: false, domain6: false} - for name, tc := range map[string]struct { - proxied mockproxied ok bool - msg string - ShouldDisplayHints map[string]bool + monitorMessages []string + notifierMessages []string prepareMockPP func(m *mocks.MockPP) prepareMockProvider mockproviders prepareMockSetter func(ppfmt pp.PP, m *mocks.MockSetter) }{ "none": { - proxiedNone, true, - ``, - allHints, + []string{}, + []string{}, nil, mockproviders{}, nil, }, "ip4only": { - proxiedNone, true, - "Deleted ip4.hello A", - allHints, + []string{"Deleted A: ip4.hello"}, + []string{"Deleted A records of ip4.hello."}, nil, mockproviders{ipnet.IP4: true}, func(ppfmt pp.PP, m *mocks.MockSetter) { m.EXPECT().Delete(gomock.Any(), ppfmt, domain4, ipnet.IP4). - Return(setter.ResponseUpdatesApplied) + Return(setter.ResponseUpdated) }, }, "ip4only/setfail": { - proxiedNone, false, - "Failed to delete ip4.hello A", - allHints, + []string{"Failed to delete A: ip4.hello"}, + []string{"Failed to finish deleting A records of ip4.hello."}, nil, mockproviders{ipnet.IP4: true}, func(ppfmt pp.PP, m *mocks.MockSetter) { - m.EXPECT().Delete(gomock.Any(), ppfmt, domain4, ipnet.IP4).Return(setter.ResponseUpdatesFailed) + m.EXPECT().Delete(gomock.Any(), ppfmt, domain4, ipnet.IP4).Return(setter.ResponseFailed) }, }, "ip6only": { - proxiedNone, true, - "Deleted ip6.hello AAAA", - allHints, + []string{"Deleted AAAA: ip6.hello"}, + []string{"Deleted AAAA records of ip6.hello."}, nil, mockproviders{ipnet.IP6: true}, func(ppfmt pp.PP, m *mocks.MockSetter) { - m.EXPECT().Delete(gomock.Any(), ppfmt, domain6, ipnet.IP6).Return(setter.ResponseUpdatesApplied) + m.EXPECT().Delete(gomock.Any(), ppfmt, domain6, ipnet.IP6).Return(setter.ResponseUpdated) }, }, "ip6only/setfail": { - proxiedNone, false, - "Failed to delete ip6.hello AAAA", - allHints, + []string{"Failed to delete AAAA: ip6.hello"}, + []string{"Failed to finish deleting AAAA records of ip6.hello."}, nil, mockproviders{ipnet.IP6: true}, func(ppfmt pp.PP, m *mocks.MockSetter) { - m.EXPECT().Delete(gomock.Any(), ppfmt, domain6, ipnet.IP6).Return(setter.ResponseUpdatesFailed) + m.EXPECT().Delete(gomock.Any(), ppfmt, domain6, ipnet.IP6).Return(setter.ResponseFailed) }, }, "both": { - proxiedNone, true, - "Deleted ip4.hello A\nDeleted ip6.hello AAAA", - allHints, + []string{"Deleted A: ip4.hello", "Deleted AAAA: ip6.hello"}, + []string{"Deleted A records of ip4.hello.", "Deleted AAAA records of ip6.hello."}, nil, mockproviders{ipnet.IP4: true, ipnet.IP6: true}, func(ppfmt pp.PP, m *mocks.MockSetter) { gomock.InOrder( - m.EXPECT().Delete(gomock.Any(), ppfmt, domain4, ipnet.IP4).Return(setter.ResponseUpdatesApplied), - m.EXPECT().Delete(gomock.Any(), ppfmt, domain6, ipnet.IP6).Return(setter.ResponseUpdatesApplied), + m.EXPECT().Delete(gomock.Any(), ppfmt, domain4, ipnet.IP4).Return(setter.ResponseUpdated), + m.EXPECT().Delete(gomock.Any(), ppfmt, domain6, ipnet.IP6).Return(setter.ResponseUpdated), ) }, }, "both/setfail1": { - proxiedNone, false, - "Failed to delete ip4.hello A", - allHints, + []string{"Failed to delete A: ip4.hello"}, + []string{"Failed to finish deleting A records of ip4.hello."}, nil, mockproviders{ipnet.IP4: true, ipnet.IP6: true}, func(ppfmt pp.PP, m *mocks.MockSetter) { gomock.InOrder( - m.EXPECT().Delete(gomock.Any(), ppfmt, domain4, ipnet.IP4).Return(setter.ResponseUpdatesFailed), - m.EXPECT().Delete(gomock.Any(), ppfmt, domain6, ipnet.IP6).Return(setter.ResponseNoUpdatesNeeded), + m.EXPECT().Delete(gomock.Any(), ppfmt, domain4, ipnet.IP4).Return(setter.ResponseFailed), + m.EXPECT().Delete(gomock.Any(), ppfmt, domain6, ipnet.IP6).Return(setter.ResponseNoop), ) }, }, "both/setfail2": { - proxiedNone, false, - "Failed to delete ip6.hello AAAA", - allHints, + []string{"Failed to delete AAAA: ip6.hello"}, + []string{"Failed to finish deleting AAAA records of ip6.hello."}, nil, mockproviders{ipnet.IP4: true, ipnet.IP6: true}, func(ppfmt pp.PP, m *mocks.MockSetter) { gomock.InOrder( - m.EXPECT().Delete(gomock.Any(), ppfmt, domain4, ipnet.IP4).Return(setter.ResponseNoUpdatesNeeded), - m.EXPECT().Delete(gomock.Any(), ppfmt, domain6, ipnet.IP6).Return(setter.ResponseUpdatesFailed), + m.EXPECT().Delete(gomock.Any(), ppfmt, domain4, ipnet.IP4).Return(setter.ResponseNoop), + m.EXPECT().Delete(gomock.Any(), ppfmt, domain6, ipnet.IP6).Return(setter.ResponseFailed), ) }, }, @@ -470,13 +769,13 @@ func TestDeleteIPs(t *testing.T) { conf := config.Default() conf.Domains = domains conf.TTL = api.TTLAuto - conf.Proxied = tc.proxied + conf.Proxied = map[domain.Domain]bool{domain4: false, domain6: false} mockPP := mocks.NewMockPP(mockCtrl) if tc.prepareMockPP != nil { tc.prepareMockPP(mockPP) } for k := range updater.ShouldDisplayHints { - updater.ShouldDisplayHints[k] = tc.ShouldDisplayHints[k] + updater.ShouldDisplayHints[k] = true } for _, ipnet := range [...]ipnet.Type{ipnet.IP4, ipnet.IP6} { if !tc.prepareMockProvider[ipnet] { @@ -490,9 +789,13 @@ func TestDeleteIPs(t *testing.T) { if tc.prepareMockSetter != nil { tc.prepareMockSetter(mockPP, mockSetter) } - ok, msg := updater.DeleteIPs(ctx, mockPP, conf, mockSetter) - require.Equal(t, tc.ok, ok) - require.Equal(t, tc.msg, msg) + resp := updater.DeleteIPs(ctx, mockPP, conf, mockSetter) + + require.Equal(t, response.Response{ + Ok: tc.ok, + NotifierMessages: tc.notifierMessages, + MonitorMessages: tc.monitorMessages, + }, resp) }) } }