From 319e6a0d266a060037c08080ceb2495df600d63f Mon Sep 17 00:00:00 2001 From: favonia Date: Fri, 30 Aug 2024 20:32:52 -0500 Subject: [PATCH] refactor: rewrite code using Go 1.23 iterators (#911) --- .golangci.yaml | 3 + cmd/ddns/ddns.go | 30 ++-- go.mod | 2 +- internal/config/config.go | 8 +- internal/config/config_print.go | 18 +-- internal/config/config_print_test.go | 10 +- internal/config/config_read.go | 6 +- internal/config/env_monitor.go | 8 +- internal/config/env_monitor_test.go | 36 ++--- internal/config/env_notifier.go | 4 +- internal/config/env_notifier_test.go | 32 ++-- internal/message/message.go | 21 ++- internal/message/monitor.go | 43 ------ internal/message/notifier.go | 22 --- internal/mocks/mock_monitor.go | 206 +++++++++++++++++--------- internal/mocks/mock_notifier.go | 13 +- internal/monitor/base.go | 54 +++---- internal/monitor/composite.go | 84 +++++------ internal/monitor/composite_test.go | 206 ++++++++++---------------- internal/monitor/healthchecks.go | 42 +++--- internal/monitor/healthchecks_test.go | 124 ++++++++-------- internal/monitor/message.go | 45 ++++++ internal/monitor/uptimekuma.go | 47 ++---- internal/monitor/uptimekuma_test.go | 95 +++--------- internal/notifier/base.go | 13 +- internal/notifier/composite.go | 37 ----- internal/notifier/composite_test.go | 83 ----------- internal/notifier/message.go | 29 ++++ internal/notifier/shoutrrr.go | 65 ++++++-- internal/notifier/shoutrrr_test.go | 91 +++++++++--- internal/updater/message.go | 26 ++-- internal/updater/message_waf.go | 22 +-- internal/updater/updater.go | 2 +- internal/updater/updater_test.go | 18 ++- 34 files changed, 727 insertions(+), 818 deletions(-) delete mode 100644 internal/message/monitor.go delete mode 100644 internal/message/notifier.go create mode 100644 internal/monitor/message.go delete mode 100644 internal/notifier/composite.go delete mode 100644 internal/notifier/composite_test.go create mode 100644 internal/notifier/message.go diff --git a/.golangci.yaml b/.golangci.yaml index 5d000326..7d6ada53 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -10,6 +10,8 @@ linters-settings: settings: printf: funcs: + - github.com/favonia/cloudflare-ddns/internal/message.NewMonitorMessagef + - github.com/favonia/cloudflare-ddns/internal/message.NewNotifierMessagef - (github.com/favonia/cloudflare-ddns/internal/pp.PP).Infof - (github.com/favonia/cloudflare-ddns/internal/pp.PP).Noticef - (github.com/favonia/cloudflare-ddns/internal/pp.PP).Hintf @@ -39,6 +41,7 @@ linters: disable: - execinquery # deprecated - gomnd # deprecated + - exportloopref # deprecated - cyclop # can detect complicated code, but never leads to actual code changes - funlen # can detect complicated code, but never leads to actual code changes diff --git a/cmd/ddns/ddns.go b/cmd/ddns/ddns.go index d412dbfa..612d24ee 100644 --- a/cmd/ddns/ddns.go +++ b/cmd/ddns/ddns.go @@ -57,8 +57,8 @@ func initConfig(ppfmt pp.PP) (*config.Config, setter.Setter, bool) { func stopUpdating(ctx context.Context, ppfmt pp.PP, c *config.Config, s setter.Setter) { if c.DeleteOnStop { msg := updater.DeleteIPs(ctx, ppfmt, c, s) - monitor.LogMessageAll(ctx, ppfmt, c.Monitors, msg.MonitorMessage) - notifier.SendMessageAll(ctx, ppfmt, c.Notifiers, msg.NotifierMessage) + c.Monitor.Log(ctx, ppfmt, msg.MonitorMessage) + c.Notifier.Send(ctx, ppfmt, msg.NotifierMessage) } } @@ -89,18 +89,18 @@ func realMain() int { //nolint:funlen // Read the config and get the handler and the setter c, s, configOK := initConfig(ppfmt) // Ping monitors regardless of whether initConfig succeeded - monitor.StartAll(ctx, ppfmt, c.Monitors, formatName()) + c.Monitor.Start(ctx, ppfmt, formatName()) // Bail out now if initConfig failed 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.") + c.Monitor.Ping(ctx, ppfmt, monitor.NewMessagef(false, "Configuration errors")) + c.Notifier.Send(ctx, ppfmt, notifier.NewMessagef( + "Cloudflare DDNS was misconfigured and could not start. Please check the logging for details.")) ppfmt.Infof(pp.EmojiBye, "Bye!") return 1 } // If UPDATE_CRON is not `@once` (not single-run mode), then send a notification to signal the start. if c.UpdateCron != nil { - notifier.SendAll(ctx, ppfmt, c.Notifiers, "Started running Cloudflare DDNS.") + c.Notifier.Send(ctx, ppfmt, notifier.NewMessagef("Started running Cloudflare DDNS.")) } // Without the following line, the quiet mode can be too quiet, and some system (Portainer) @@ -122,11 +122,11 @@ func realMain() int { //nolint:funlen // Update the IP addresses if first && !c.UpdateOnStart { - monitor.SuccessAll(ctx, ppfmt, c.Monitors, "Started (no action)") + c.Monitor.Ping(ctx, ppfmt, monitor.NewMessagef(true, "Started (no action)")) } else { msg := updater.UpdateIPs(ctxWithSignals, ppfmt, c, s) - monitor.PingMessageAll(ctx, ppfmt, c.Monitors, msg.MonitorMessage) - notifier.SendMessageAll(ctx, ppfmt, c.Notifiers, msg.NotifierMessage) + c.Monitor.Ping(ctx, ppfmt, msg.MonitorMessage) + c.Notifier.Send(ctx, ppfmt, msg.NotifierMessage) } if ctxWithSignals.Err() != nil { @@ -148,9 +148,9 @@ func realMain() int { //nolint:funlen cron.DescribeSchedule(c.UpdateCron), ) stopUpdating(ctx, ppfmt, c, s) - monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 1, "No scheduled updates") - notifier.SendAll(ctx, ppfmt, c.Notifiers, - fmt.Sprintf( + c.Monitor.Ping(ctx, ppfmt, monitor.NewMessagef(false, "No scheduled updates")) + c.Notifier.Send(ctx, ppfmt, + notifier.NewMessagef( "Cloudflare DDNS stopped because there are no scheduled updates in near future. "+ "Consider changing the value of UPDATE_CRON (%s).", cron.DescribeSchedule(c.UpdateCron), @@ -167,9 +167,9 @@ func realMain() int { //nolint:funlen // Wait for the next signal or the alarm, whichever comes first if sig.WaitForSignalsUntil(ppfmt, next) { stopUpdating(ctx, ppfmt, c, s) - monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 0, "Stopped") + c.Monitor.Exit(ctx, ppfmt, "Stopped") if c.UpdateCron != nil { - notifier.SendAll(ctx, ppfmt, c.Notifiers, "Stopped running Cloudflare DDNS.") + c.Notifier.Send(ctx, ppfmt, notifier.NewMessagef("Stopped running Cloudflare DDNS.")) } ppfmt.Infof(pp.EmojiBye, "Bye!") return 0 diff --git a/go.mod b/go.mod index 1106fd9c..e8e86e9e 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/stretchr/testify v1.9.0 go.uber.org/mock v0.4.0 golang.org/x/net v0.28.0 + golang.org/x/text v0.17.0 ) require ( @@ -24,7 +25,6 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.23.0 // indirect - golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.5.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/internal/config/config.go b/internal/config/config.go index ab26a8ea..0b221102 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -31,8 +31,8 @@ type Config struct { WAFListDescription string DetectionTimeout time.Duration UpdateTimeout time.Duration - Monitors []monitor.Monitor - Notifiers []notifier.Notifier + Monitor monitor.Monitor + Notifier notifier.Notifier } // Default gives the default configuration. @@ -59,7 +59,7 @@ func Default() *Config { WAFListDescription: "", DetectionTimeout: time.Second * 5, //nolint:mnd UpdateTimeout: time.Second * 30, //nolint:mnd - Monitors: nil, - Notifiers: nil, + Monitor: nil, + Notifier: nil, } } diff --git a/internal/config/config_print.go b/internal/config/config_print.go index 955534e0..52740979 100644 --- a/internal/config/config_print.go +++ b/internal/config/config_print.go @@ -10,8 +10,6 @@ import ( "github.com/favonia/cloudflare-ddns/internal/cron" "github.com/favonia/cloudflare-ddns/internal/domain" "github.com/favonia/cloudflare-ddns/internal/ipnet" - "github.com/favonia/cloudflare-ddns/internal/monitor" - "github.com/favonia/cloudflare-ddns/internal/notifier" "github.com/favonia/cloudflare-ddns/internal/pp" "github.com/favonia/cloudflare-ddns/internal/provider" ) @@ -88,17 +86,17 @@ func (c *Config) Print(ppfmt pp.PP) { item("IP detection:", "%v", c.DetectionTimeout) item("Record/list updating:", "%v", c.UpdateTimeout) - if len(c.Monitors) > 0 { + if c.Monitor != nil { section("Monitors:") - monitor.DescribeAll(func(service, params string) { - item(service+":", "%s", params) - }, c.Monitors) + for name, params := range c.Monitor.Describe { + item(name+":", "%s", params) + } } - if len(c.Notifiers) > 0 { + if c.Notifier != nil { section("Notification services (via shoutrrr):") - notifier.DescribeAll(func(service, params string) { - item(service+":", "%s", params) - }, c.Notifiers) + for name, params := range c.Notifier.Describe { + item(name+":", "%s", params) + } } } diff --git a/internal/config/config_print_test.go b/internal/config/config_print_test.go index d3ae178a..a703b52e 100644 --- a/internal/config/config_print_test.go +++ b/internal/config/config_print_test.go @@ -9,8 +9,6 @@ import ( "github.com/favonia/cloudflare-ddns/internal/domain" "github.com/favonia/cloudflare-ddns/internal/ipnet" "github.com/favonia/cloudflare-ddns/internal/mocks" - "github.com/favonia/cloudflare-ddns/internal/monitor" - "github.com/favonia/cloudflare-ddns/internal/notifier" "github.com/favonia/cloudflare-ddns/internal/pp" ) @@ -114,17 +112,17 @@ func TestPrintValues(t *testing.T) { m := mocks.NewMockMonitor(mockCtrl) m.EXPECT().Describe(gomock.Any()). - DoAndReturn(func(f func(string, string)) { + DoAndReturn(func(f func(string, string) bool) { f("Meow", "purrrr") }).AnyTimes() - c.Monitors = []monitor.Monitor{m} + c.Monitor = m n := mocks.NewMockNotifier(mockCtrl) n.EXPECT().Describe(gomock.Any()). - DoAndReturn(func(f func(string, string)) { + DoAndReturn(func(f func(string, string) bool) { f("Snake", "hissss") }).AnyTimes() - c.Notifiers = []notifier.Notifier{n} + c.Notifier = n c.Print(mockPP) } diff --git a/internal/config/config_read.go b/internal/config/config_read.go index 54870e52..d0f40e08 100644 --- a/internal/config/config_read.go +++ b/internal/config/config_read.go @@ -34,9 +34,9 @@ func (c *Config) ReadEnv(ppfmt pp.PP) bool { !ReadString(ppfmt, "WAF_LIST_DESCRIPTION", &c.WAFListDescription) || !ReadNonnegDuration(ppfmt, "DETECTION_TIMEOUT", &c.DetectionTimeout) || !ReadNonnegDuration(ppfmt, "UPDATE_TIMEOUT", &c.UpdateTimeout) || - !ReadAndAppendHealthchecksURL(ppfmt, "HEALTHCHECKS", &c.Monitors) || - !ReadAndAppendUptimeKumaURL(ppfmt, "UPTIMEKUMA", &c.Monitors) || - !ReadAndAppendShoutrrrURL(ppfmt, "SHOUTRRR", &c.Notifiers) { + !ReadAndAppendHealthchecksURL(ppfmt, "HEALTHCHECKS", &c.Monitor) || + !ReadAndAppendUptimeKumaURL(ppfmt, "UPTIMEKUMA", &c.Monitor) || + !ReadAndAppendShoutrrrURL(ppfmt, "SHOUTRRR", &c.Notifier) { return false } diff --git a/internal/config/env_monitor.go b/internal/config/env_monitor.go index d8643e7c..ea547bad 100644 --- a/internal/config/env_monitor.go +++ b/internal/config/env_monitor.go @@ -6,7 +6,7 @@ import ( ) // ReadAndAppendHealthchecksURL reads the base URL of a Healthchecks endpoint. -func ReadAndAppendHealthchecksURL(ppfmt pp.PP, key string, field *[]monitor.Monitor) bool { +func ReadAndAppendHealthchecksURL(ppfmt pp.PP, key string, field *monitor.Monitor) bool { val := Getenv(key) if val == "" { @@ -19,12 +19,12 @@ func ReadAndAppendHealthchecksURL(ppfmt pp.PP, key string, field *[]monitor.Moni } // Append the new monitor to the existing list - *field = append(*field, h) + *field = monitor.NewComposed(*field, h) return true } // ReadAndAppendUptimeKumaURL reads the URL of a Push Monitor of an Uptime Kuma server. -func ReadAndAppendUptimeKumaURL(ppfmt pp.PP, key string, field *[]monitor.Monitor) bool { +func ReadAndAppendUptimeKumaURL(ppfmt pp.PP, key string, field *monitor.Monitor) bool { val := Getenv(key) if val == "" { @@ -37,6 +37,6 @@ func ReadAndAppendUptimeKumaURL(ppfmt pp.PP, key string, field *[]monitor.Monito } // Append the new monitor to the existing list - *field = append(*field, h) + *field = monitor.NewComposed(*field, h) return true } diff --git a/internal/config/env_monitor_test.go b/internal/config/env_monitor_test.go index c47e9744..16e9cd5b 100644 --- a/internal/config/env_monitor_test.go +++ b/internal/config/env_monitor_test.go @@ -21,8 +21,8 @@ func TestReadAndAppendHealthchecksURL(t *testing.T) { for name, tc := range map[string]struct { set bool val string - oldField []mon - newField []mon + oldField mon + newField mon ok bool prepareMockPP func(*mocks.MockPP) }{ @@ -35,30 +35,30 @@ func TestReadAndAppendHealthchecksURL(t *testing.T) { "example": { true, "https://hi.org/1234", nil, - []mon{monitor.Healthchecks{ + monitor.NewComposed(monitor.Healthchecks{ BaseURL: urlMustParse(t, "https://hi.org/1234"), Timeout: monitor.HealthchecksDefaultTimeout, - }}, + }), true, nil, }, "password": { true, "https://me:pass@hi.org/1234", nil, - []mon{monitor.Healthchecks{ + monitor.NewComposed(monitor.Healthchecks{ BaseURL: urlMustParse(t, "https://me:pass@hi.org/1234"), Timeout: monitor.HealthchecksDefaultTimeout, - }}, + }), true, nil, }, "fragment": { true, "https://hi.org/1234#fragment", nil, - []mon{monitor.Healthchecks{ + monitor.NewComposed(monitor.Healthchecks{ BaseURL: urlMustParse(t, "https://hi.org/1234#fragment"), Timeout: monitor.HealthchecksDefaultTimeout, - }}, + }), true, nil, }, @@ -118,8 +118,8 @@ func TestReadAndAppendUptimeKumaURL(t *testing.T) { for name, tc := range map[string]struct { set bool val string - oldField []mon - newField []mon + oldField mon + newField mon ok bool prepareMockPP func(*mocks.MockPP) }{ @@ -132,40 +132,40 @@ func TestReadAndAppendUptimeKumaURL(t *testing.T) { "example": { true, "https://hi.org/1234", nil, - []mon{monitor.UptimeKuma{ + monitor.NewComposed(monitor.UptimeKuma{ BaseURL: urlMustParse(t, "https://hi.org/1234"), Timeout: monitor.UptimeKumaDefaultTimeout, - }}, + }), true, nil, }, "password": { true, "https://me:pass@hi.org/1234", nil, - []mon{monitor.UptimeKuma{ + monitor.NewComposed(monitor.UptimeKuma{ BaseURL: urlMustParse(t, "https://me:pass@hi.org/1234"), Timeout: monitor.UptimeKumaDefaultTimeout, - }}, + }), true, nil, }, "fragment": { true, "https://hi.org/1234#fragment", nil, - []mon{monitor.UptimeKuma{ + monitor.NewComposed(monitor.UptimeKuma{ BaseURL: urlMustParse(t, "https://hi.org/1234#fragment"), Timeout: monitor.UptimeKumaDefaultTimeout, - }}, + }), true, nil, }, "query": { true, "https://hi.org/1234?hello=123", nil, - []mon{monitor.UptimeKuma{ + monitor.NewComposed(monitor.UptimeKuma{ BaseURL: urlMustParse(t, "https://hi.org/1234"), Timeout: monitor.UptimeKumaDefaultTimeout, - }}, + }), true, func(m *mocks.MockPP) { m.EXPECT().Noticef(pp.EmojiUserError, diff --git a/internal/config/env_notifier.go b/internal/config/env_notifier.go index 89b91b0c..0da43daa 100644 --- a/internal/config/env_notifier.go +++ b/internal/config/env_notifier.go @@ -6,7 +6,7 @@ import ( ) // ReadAndAppendShoutrrrURL reads the URLs separated by the newline. -func ReadAndAppendShoutrrrURL(ppfmt pp.PP, key string, field *[]notifier.Notifier) bool { +func ReadAndAppendShoutrrrURL(ppfmt pp.PP, key string, field *notifier.Notifier) bool { vals := GetenvAsList(key, "\n") if len(vals) == 0 { return true @@ -18,6 +18,6 @@ func ReadAndAppendShoutrrrURL(ppfmt pp.PP, key string, field *[]notifier.Notifie } // Append the new monitor to the existing list - *field = append(*field, s) + *field = s return true } diff --git a/internal/config/env_notifier_test.go b/internal/config/env_notifier_test.go index 751634b1..6002c909 100644 --- a/internal/config/env_notifier_test.go +++ b/internal/config/env_notifier_test.go @@ -21,37 +21,35 @@ func TestReadAndAppendShoutrrrURL(t *testing.T) { for name, tc := range map[string]struct { set bool val string - oldField []not - newField func(*testing.T, []not) + oldField not + newField func(*testing.T, not) ok bool prepareMockPP func(*mocks.MockPP) }{ "unset": { false, "", nil, - func(t *testing.T, ns []not) { + func(t *testing.T, n not) { t.Helper() - require.Nil(t, ns) + require.Nil(t, n) }, true, nil, }, "empty": { true, "", nil, - func(t *testing.T, ns []not) { + func(t *testing.T, n not) { t.Helper() - require.Nil(t, ns) + require.Nil(t, n) }, true, nil, }, "generic": { true, "generic+https://example.com/api/v1/postStuff", nil, - func(t *testing.T, ns []not) { + func(t *testing.T, n not) { t.Helper() - require.Len(t, ns, 1) - m := ns[0] - s, ok := m.(notifier.Shoutrrr) + s, ok := n.(notifier.Shoutrrr) require.True(t, ok) - require.Equal(t, []string{"generic"}, s.ServiceNames) + require.Equal(t, []string{"Generic"}, s.ServiceDescriptions) }, true, nil, @@ -59,9 +57,9 @@ func TestReadAndAppendShoutrrrURL(t *testing.T) { "ill-formed": { true, "meow-meow-meow://cute", nil, - func(t *testing.T, ns []not) { + func(t *testing.T, n not) { t.Helper() - require.Nil(t, ns) + require.Nil(t, n) }, false, func(m *mocks.MockPP) { @@ -71,13 +69,11 @@ func TestReadAndAppendShoutrrrURL(t *testing.T) { "multiple": { true, "generic+https://example.com/api/v1/postStuff\npushover://shoutrrr:token@userKey", nil, - func(t *testing.T, ns []not) { + func(t *testing.T, n not) { t.Helper() - require.Len(t, ns, 1) - m := ns[0] - s, ok := m.(notifier.Shoutrrr) + s, ok := n.(notifier.Shoutrrr) require.True(t, ok) - require.Equal(t, []string{"generic", "pushover"}, s.ServiceNames) + require.Equal(t, []string{"Generic", "Pushover"}, s.ServiceDescriptions) }, true, nil, diff --git a/internal/message/message.go b/internal/message/message.go index 41f30f91..61e63f26 100644 --- a/internal/message/message.go +++ b/internal/message/message.go @@ -2,24 +2,29 @@ // monitors and notifiers. package message +import ( + "github.com/favonia/cloudflare-ddns/internal/monitor" + "github.com/favonia/cloudflare-ddns/internal/notifier" +) + // Message encapsulates the messages to both monitiors and notifiers. type Message struct { - NotifierMessage - MonitorMessage + MonitorMessage monitor.Message + NotifierMessage notifier.Message } // New creates a new, empty message. func New() Message { return Message{ - MonitorMessage: NewMonitorMessage(), - NotifierMessage: NewNotifierMessage(), + MonitorMessage: monitor.NewMessage(), + NotifierMessage: notifier.NewMessage(), } } // Merge combines multiple compound messages. func Merge(msgs ...Message) Message { - mms := make([]MonitorMessage, len(msgs)) - nms := make([]NotifierMessage, len(msgs)) + mms := make([]monitor.Message, len(msgs)) + nms := make([]notifier.Message, len(msgs)) for i := range msgs { mms[i] = msgs[i].MonitorMessage @@ -27,7 +32,7 @@ func Merge(msgs ...Message) Message { } return Message{ - MonitorMessage: MergeMonitorMessages(mms...), - NotifierMessage: MergeNotifierMessages(nms...), + MonitorMessage: monitor.MergeMessages(mms...), + NotifierMessage: notifier.MergeMessages(nms...), } } diff --git a/internal/message/monitor.go b/internal/message/monitor.go deleted file mode 100644 index 5b350cb6..00000000 --- a/internal/message/monitor.go +++ /dev/null @@ -1,43 +0,0 @@ -package message - -import ( - "slices" - "strings" -) - -// MonitorMessage holds the messages and success/failure status for monitors. -type MonitorMessage struct { - OK bool - Lines []string -} - -// NewMonitorMessage creates a new empty MonitorMessage. -func NewMonitorMessage() MonitorMessage { - return MonitorMessage{ - OK: true, - Lines: nil, - } -} - -// Format turns the message into a single string. -func (m MonitorMessage) Format() string { - return strings.Join(m.Lines, "\n") -} - -// MergeMonitorMessages keeps only the ones with highest severity. -func MergeMonitorMessages(msgs ...MonitorMessage) MonitorMessage { - var ( - OK = true - Lines = map[bool][][]string{} - ) - - for _, msg := range msgs { - OK = OK && msg.OK - Lines[msg.OK] = append(Lines[msg.OK], msg.Lines) - } - - return MonitorMessage{ - OK: OK, - Lines: slices.Concat(Lines[OK]...), - } -} diff --git a/internal/message/notifier.go b/internal/message/notifier.go deleted file mode 100644 index 72aac104..00000000 --- a/internal/message/notifier.go +++ /dev/null @@ -1,22 +0,0 @@ -package message - -import ( - "slices" - "strings" -) - -// NotifierMessage holds the messages and success/failure status for notifiers. -type NotifierMessage []string - -// NewNotifierMessage creates a new empty NotifierMessage. -func NewNotifierMessage() NotifierMessage { return nil } - -// MergeNotifierMessages keeps only the ones with highest severity. -func MergeNotifierMessages(msgs ...NotifierMessage) NotifierMessage { - return slices.Concat[NotifierMessage, string](msgs...) -} - -// Format turns the message into a single string. -func (m NotifierMessage) Format() string { - return strings.Join(m, " ") -} diff --git a/internal/mocks/mock_monitor.go b/internal/mocks/mock_monitor.go index dde6290e..573d5919 100644 --- a/internal/mocks/mock_monitor.go +++ b/internal/mocks/mock_monitor.go @@ -1,9 +1,9 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/favonia/cloudflare-ddns/internal/monitor (interfaces: Monitor) +// Source: github.com/favonia/cloudflare-ddns/internal/monitor (interfaces: BasicMonitor,Monitor) // // Generated by this command: // -// mockgen -typed -destination=../mocks/mock_monitor.go -package=mocks . Monitor +// mockgen -typed -destination=../mocks/mock_monitor.go -package=mocks . BasicMonitor,Monitor // // Package mocks is a generated GoMock package. package mocks @@ -12,147 +12,207 @@ import ( context "context" reflect "reflect" + monitor "github.com/favonia/cloudflare-ddns/internal/monitor" pp "github.com/favonia/cloudflare-ddns/internal/pp" gomock "go.uber.org/mock/gomock" ) -// MockMonitor is a mock of Monitor interface. -type MockMonitor struct { +// MockBasicMonitor is a mock of BasicMonitor interface. +type MockBasicMonitor struct { ctrl *gomock.Controller - recorder *MockMonitorMockRecorder + recorder *MockBasicMonitorMockRecorder } -// MockMonitorMockRecorder is the mock recorder for MockMonitor. -type MockMonitorMockRecorder struct { - mock *MockMonitor +// MockBasicMonitorMockRecorder is the mock recorder for MockBasicMonitor. +type MockBasicMonitorMockRecorder struct { + mock *MockBasicMonitor } -// NewMockMonitor creates a new mock instance. -func NewMockMonitor(ctrl *gomock.Controller) *MockMonitor { - mock := &MockMonitor{ctrl: ctrl} - mock.recorder = &MockMonitorMockRecorder{mock} +// NewMockBasicMonitor creates a new mock instance. +func NewMockBasicMonitor(ctrl *gomock.Controller) *MockBasicMonitor { + mock := &MockBasicMonitor{ctrl: ctrl} + mock.recorder = &MockBasicMonitorMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockMonitor) EXPECT() *MockMonitorMockRecorder { +func (m *MockBasicMonitor) EXPECT() *MockBasicMonitorMockRecorder { return m.recorder } // Describe mocks base method. -func (m *MockMonitor) Describe(arg0 func(string, string)) { +func (m *MockBasicMonitor) Describe(arg0 func(string, string) bool) { m.ctrl.T.Helper() m.ctrl.Call(m, "Describe", arg0) } // Describe indicates an expected call of Describe. -func (mr *MockMonitorMockRecorder) Describe(arg0 any) *MonitorDescribeCall { +func (mr *MockBasicMonitorMockRecorder) Describe(arg0 any) *BasicMonitorDescribeCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Describe", reflect.TypeOf((*MockMonitor)(nil).Describe), arg0) - return &MonitorDescribeCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Describe", reflect.TypeOf((*MockBasicMonitor)(nil).Describe), arg0) + return &BasicMonitorDescribeCall{Call: call} } -// MonitorDescribeCall wrap *gomock.Call -type MonitorDescribeCall struct { +// BasicMonitorDescribeCall wrap *gomock.Call +type BasicMonitorDescribeCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MonitorDescribeCall) Return() *MonitorDescribeCall { +func (c *BasicMonitorDescribeCall) Return() *BasicMonitorDescribeCall { c.Call = c.Call.Return() return c } // Do rewrite *gomock.Call.Do -func (c *MonitorDescribeCall) Do(f func(func(string, string))) *MonitorDescribeCall { +func (c *BasicMonitorDescribeCall) Do(f func(func(string, string) bool)) *BasicMonitorDescribeCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MonitorDescribeCall) DoAndReturn(f func(func(string, string))) *MonitorDescribeCall { +func (c *BasicMonitorDescribeCall) DoAndReturn(f func(func(string, string) bool)) *BasicMonitorDescribeCall { c.Call = c.Call.DoAndReturn(f) return c } -// ExitStatus mocks base method. -func (m *MockMonitor) ExitStatus(arg0 context.Context, arg1 pp.PP, arg2 int, arg3 string) bool { +// Ping mocks base method. +func (m *MockBasicMonitor) Ping(arg0 context.Context, arg1 pp.PP, arg2 monitor.Message) bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ExitStatus", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "Ping", arg0, arg1, arg2) ret0, _ := ret[0].(bool) return ret0 } -// ExitStatus indicates an expected call of ExitStatus. -func (mr *MockMonitorMockRecorder) ExitStatus(arg0, arg1, arg2, arg3 any) *MonitorExitStatusCall { +// Ping indicates an expected call of Ping. +func (mr *MockBasicMonitorMockRecorder) Ping(arg0, arg1, arg2 any) *BasicMonitorPingCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExitStatus", reflect.TypeOf((*MockMonitor)(nil).ExitStatus), arg0, arg1, arg2, arg3) - return &MonitorExitStatusCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockBasicMonitor)(nil).Ping), arg0, arg1, arg2) + return &BasicMonitorPingCall{Call: call} } -// MonitorExitStatusCall wrap *gomock.Call -type MonitorExitStatusCall struct { +// BasicMonitorPingCall wrap *gomock.Call +type BasicMonitorPingCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MonitorExitStatusCall) Return(arg0 bool) *MonitorExitStatusCall { +func (c *BasicMonitorPingCall) Return(arg0 bool) *BasicMonitorPingCall { c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do -func (c *MonitorExitStatusCall) Do(f func(context.Context, pp.PP, int, string) bool) *MonitorExitStatusCall { +func (c *BasicMonitorPingCall) Do(f func(context.Context, pp.PP, monitor.Message) bool) *BasicMonitorPingCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MonitorExitStatusCall) DoAndReturn(f func(context.Context, pp.PP, int, string) bool) *MonitorExitStatusCall { +func (c *BasicMonitorPingCall) DoAndReturn(f func(context.Context, pp.PP, monitor.Message) bool) *BasicMonitorPingCall { c.Call = c.Call.DoAndReturn(f) return c } -// Failure mocks base method. -func (m *MockMonitor) Failure(arg0 context.Context, arg1 pp.PP, arg2 string) bool { +// MockMonitor is a mock of Monitor interface. +type MockMonitor struct { + ctrl *gomock.Controller + recorder *MockMonitorMockRecorder +} + +// MockMonitorMockRecorder is the mock recorder for MockMonitor. +type MockMonitorMockRecorder struct { + mock *MockMonitor +} + +// NewMockMonitor creates a new mock instance. +func NewMockMonitor(ctrl *gomock.Controller) *MockMonitor { + mock := &MockMonitor{ctrl: ctrl} + mock.recorder = &MockMonitorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockMonitor) EXPECT() *MockMonitorMockRecorder { + return m.recorder +} + +// Describe mocks base method. +func (m *MockMonitor) Describe(arg0 func(string, string) bool) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Failure", arg0, arg1, arg2) + m.ctrl.Call(m, "Describe", arg0) +} + +// Describe indicates an expected call of Describe. +func (mr *MockMonitorMockRecorder) Describe(arg0 any) *MonitorDescribeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Describe", reflect.TypeOf((*MockMonitor)(nil).Describe), arg0) + return &MonitorDescribeCall{Call: call} +} + +// MonitorDescribeCall wrap *gomock.Call +type MonitorDescribeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MonitorDescribeCall) Return() *MonitorDescribeCall { + c.Call = c.Call.Return() + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MonitorDescribeCall) Do(f func(func(string, string) bool)) *MonitorDescribeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MonitorDescribeCall) DoAndReturn(f func(func(string, string) bool)) *MonitorDescribeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Exit mocks base method. +func (m *MockMonitor) Exit(arg0 context.Context, arg1 pp.PP, arg2 string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Exit", arg0, arg1, arg2) ret0, _ := ret[0].(bool) return ret0 } -// Failure indicates an expected call of Failure. -func (mr *MockMonitorMockRecorder) Failure(arg0, arg1, arg2 any) *MonitorFailureCall { +// Exit indicates an expected call of Exit. +func (mr *MockMonitorMockRecorder) Exit(arg0, arg1, arg2 any) *MonitorExitCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Failure", reflect.TypeOf((*MockMonitor)(nil).Failure), arg0, arg1, arg2) - return &MonitorFailureCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exit", reflect.TypeOf((*MockMonitor)(nil).Exit), arg0, arg1, arg2) + return &MonitorExitCall{Call: call} } -// MonitorFailureCall wrap *gomock.Call -type MonitorFailureCall struct { +// MonitorExitCall wrap *gomock.Call +type MonitorExitCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MonitorFailureCall) Return(arg0 bool) *MonitorFailureCall { +func (c *MonitorExitCall) Return(arg0 bool) *MonitorExitCall { c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do -func (c *MonitorFailureCall) Do(f func(context.Context, pp.PP, string) bool) *MonitorFailureCall { +func (c *MonitorExitCall) Do(f func(context.Context, pp.PP, string) bool) *MonitorExitCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MonitorFailureCall) DoAndReturn(f func(context.Context, pp.PP, string) bool) *MonitorFailureCall { +func (c *MonitorExitCall) DoAndReturn(f func(context.Context, pp.PP, string) bool) *MonitorExitCall { c.Call = c.Call.DoAndReturn(f) return c } // Log mocks base method. -func (m *MockMonitor) Log(arg0 context.Context, arg1 pp.PP, arg2 string) bool { +func (m *MockMonitor) Log(arg0 context.Context, arg1 pp.PP, arg2 monitor.Message) bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Log", arg0, arg1, arg2) ret0, _ := ret[0].(bool) @@ -178,89 +238,89 @@ func (c *MonitorLogCall) Return(arg0 bool) *MonitorLogCall { } // Do rewrite *gomock.Call.Do -func (c *MonitorLogCall) Do(f func(context.Context, pp.PP, string) bool) *MonitorLogCall { +func (c *MonitorLogCall) Do(f func(context.Context, pp.PP, monitor.Message) bool) *MonitorLogCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MonitorLogCall) DoAndReturn(f func(context.Context, pp.PP, string) bool) *MonitorLogCall { +func (c *MonitorLogCall) DoAndReturn(f func(context.Context, pp.PP, monitor.Message) bool) *MonitorLogCall { c.Call = c.Call.DoAndReturn(f) return c } -// Start mocks base method. -func (m *MockMonitor) Start(arg0 context.Context, arg1 pp.PP, arg2 string) bool { +// Ping mocks base method. +func (m *MockMonitor) Ping(arg0 context.Context, arg1 pp.PP, arg2 monitor.Message) bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Start", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "Ping", arg0, arg1, arg2) ret0, _ := ret[0].(bool) return ret0 } -// Start indicates an expected call of Start. -func (mr *MockMonitorMockRecorder) Start(arg0, arg1, arg2 any) *MonitorStartCall { +// Ping indicates an expected call of Ping. +func (mr *MockMonitorMockRecorder) Ping(arg0, arg1, arg2 any) *MonitorPingCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockMonitor)(nil).Start), arg0, arg1, arg2) - return &MonitorStartCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockMonitor)(nil).Ping), arg0, arg1, arg2) + return &MonitorPingCall{Call: call} } -// MonitorStartCall wrap *gomock.Call -type MonitorStartCall struct { +// MonitorPingCall wrap *gomock.Call +type MonitorPingCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MonitorStartCall) Return(arg0 bool) *MonitorStartCall { +func (c *MonitorPingCall) Return(arg0 bool) *MonitorPingCall { c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do -func (c *MonitorStartCall) Do(f func(context.Context, pp.PP, string) bool) *MonitorStartCall { +func (c *MonitorPingCall) Do(f func(context.Context, pp.PP, monitor.Message) bool) *MonitorPingCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MonitorStartCall) DoAndReturn(f func(context.Context, pp.PP, string) bool) *MonitorStartCall { +func (c *MonitorPingCall) DoAndReturn(f func(context.Context, pp.PP, monitor.Message) bool) *MonitorPingCall { c.Call = c.Call.DoAndReturn(f) return c } -// Success mocks base method. -func (m *MockMonitor) Success(arg0 context.Context, arg1 pp.PP, arg2 string) bool { +// Start mocks base method. +func (m *MockMonitor) Start(arg0 context.Context, arg1 pp.PP, arg2 string) bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Success", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "Start", arg0, arg1, arg2) ret0, _ := ret[0].(bool) return ret0 } -// Success indicates an expected call of Success. -func (mr *MockMonitorMockRecorder) Success(arg0, arg1, arg2 any) *MonitorSuccessCall { +// Start indicates an expected call of Start. +func (mr *MockMonitorMockRecorder) Start(arg0, arg1, arg2 any) *MonitorStartCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Success", reflect.TypeOf((*MockMonitor)(nil).Success), arg0, arg1, arg2) - return &MonitorSuccessCall{Call: call} + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockMonitor)(nil).Start), arg0, arg1, arg2) + return &MonitorStartCall{Call: call} } -// MonitorSuccessCall wrap *gomock.Call -type MonitorSuccessCall struct { +// MonitorStartCall wrap *gomock.Call +type MonitorStartCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *MonitorSuccessCall) Return(arg0 bool) *MonitorSuccessCall { +func (c *MonitorStartCall) Return(arg0 bool) *MonitorStartCall { c.Call = c.Call.Return(arg0) return c } // Do rewrite *gomock.Call.Do -func (c *MonitorSuccessCall) Do(f func(context.Context, pp.PP, string) bool) *MonitorSuccessCall { +func (c *MonitorStartCall) Do(f func(context.Context, pp.PP, string) bool) *MonitorStartCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MonitorSuccessCall) DoAndReturn(f func(context.Context, pp.PP, string) bool) *MonitorSuccessCall { +func (c *MonitorStartCall) DoAndReturn(f func(context.Context, pp.PP, string) bool) *MonitorStartCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/internal/mocks/mock_notifier.go b/internal/mocks/mock_notifier.go index 74585b2a..8186c81a 100644 --- a/internal/mocks/mock_notifier.go +++ b/internal/mocks/mock_notifier.go @@ -12,6 +12,7 @@ import ( context "context" reflect "reflect" + notifier "github.com/favonia/cloudflare-ddns/internal/notifier" pp "github.com/favonia/cloudflare-ddns/internal/pp" gomock "go.uber.org/mock/gomock" ) @@ -40,7 +41,7 @@ func (m *MockNotifier) EXPECT() *MockNotifierMockRecorder { } // Describe mocks base method. -func (m *MockNotifier) Describe(arg0 func(string, string)) { +func (m *MockNotifier) Describe(arg0 func(string, string) bool) { m.ctrl.T.Helper() m.ctrl.Call(m, "Describe", arg0) } @@ -64,19 +65,19 @@ func (c *NotifierDescribeCall) Return() *NotifierDescribeCall { } // Do rewrite *gomock.Call.Do -func (c *NotifierDescribeCall) Do(f func(func(string, string))) *NotifierDescribeCall { +func (c *NotifierDescribeCall) Do(f func(func(string, string) bool)) *NotifierDescribeCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *NotifierDescribeCall) DoAndReturn(f func(func(string, string))) *NotifierDescribeCall { +func (c *NotifierDescribeCall) DoAndReturn(f func(func(string, string) bool)) *NotifierDescribeCall { c.Call = c.Call.DoAndReturn(f) return c } // Send mocks base method. -func (m *MockNotifier) Send(arg0 context.Context, arg1 pp.PP, arg2 string) bool { +func (m *MockNotifier) Send(arg0 context.Context, arg1 pp.PP, arg2 notifier.Message) bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Send", arg0, arg1, arg2) ret0, _ := ret[0].(bool) @@ -102,13 +103,13 @@ func (c *NotifierSendCall) Return(arg0 bool) *NotifierSendCall { } // Do rewrite *gomock.Call.Do -func (c *NotifierSendCall) Do(f func(context.Context, pp.PP, string) bool) *NotifierSendCall { +func (c *NotifierSendCall) Do(f func(context.Context, pp.PP, notifier.Message) bool) *NotifierSendCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *NotifierSendCall) DoAndReturn(f func(context.Context, pp.PP, string) bool) *NotifierSendCall { +func (c *NotifierSendCall) DoAndReturn(f func(context.Context, pp.PP, notifier.Message) bool) *NotifierSendCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/internal/monitor/base.go b/internal/monitor/base.go index 0e45635c..e7d4f187 100644 --- a/internal/monitor/base.go +++ b/internal/monitor/base.go @@ -4,54 +4,36 @@ package monitor import ( "context" - "github.com/favonia/cloudflare-ddns/internal/message" "github.com/favonia/cloudflare-ddns/internal/pp" ) -//go:generate mockgen -typed -destination=../mocks/mock_monitor.go -package=mocks . Monitor +//go:generate mockgen -typed -destination=../mocks/mock_monitor.go -package=mocks . BasicMonitor,Monitor // maxReadLength is the maximum number of bytes read from an HTTP response. const maxReadLength int64 = 102400 -// Monitor is a dead man's switch, meaning that the user will be notified when the updater fails to +// BasicMonitor is a dead man's switch, meaning that the user will be notified when the updater fails to // detect and update the public IP address. No notifications for IP changes. -type Monitor interface { - // Describe a monitor in a human-readable format by calling callback with service names and params. - Describe(callback func(service, params string)) +type BasicMonitor interface { + // Describe a monitor as a service name and its parameters. + Describe(yield func(service, params string) bool) - // Success pings the monitor to prevent notifications. - Success(ctx context.Context, ppfmt pp.PP, message string) bool + // Ping with OK=true prevent notifications. + // Ping with OK=false immediately notifies the user. + Ping(ctx context.Context, ppfmt pp.PP, msg Message) bool +} + +// Monitor provides more advanced features. +type Monitor interface { + BasicMonitor // Start pings the monitor with the start signal. Start(ctx context.Context, ppfmt pp.PP, message string) bool - // Failure immediately signals the monitor to notify the user. - Failure(ctx context.Context, ppfmt pp.PP, message string) bool - - // Log provides additional inforamion without changing the state. - Log(ctx context.Context, ppfmt pp.PP, message string) bool - - // ExitStatus records the exit status (as an integer in the POSIX style). - ExitStatus(ctx context.Context, ppfmt pp.PP, code int, message string) bool -} - -// PingMessage formats and pings with a [message.Message]. -func PingMessage(ctx context.Context, ppfmt pp.PP, m Monitor, msg message.MonitorMessage) bool { - if msg.OK { - return m.Success(ctx, ppfmt, msg.Format()) - } else { - return m.Failure(ctx, ppfmt, msg.Format()) - } -} + // Exit pings the monitor with the successful exiting signal. + Exit(ctx context.Context, ppfmt pp.PP, message string) bool -// LogMessage formats and logs a [message.Message]. -func LogMessage(ctx context.Context, ppfmt pp.PP, m Monitor, msg message.MonitorMessage) bool { - switch { - case !msg.OK: - return m.Failure(ctx, ppfmt, msg.Format()) - case len(msg.Lines) > 0: - return m.Log(ctx, ppfmt, msg.Format()) - default: - return true - } + // Log with OK=true provides additional information without changing the state. + // Log with OK=false immediately notifies the user. + Log(ctx context.Context, ppfmt pp.PP, msg Message) bool } diff --git a/internal/monitor/composite.go b/internal/monitor/composite.go index c4a4ec9d..afb20b0a 100644 --- a/internal/monitor/composite.go +++ b/internal/monitor/composite.go @@ -3,89 +3,79 @@ package monitor import ( "context" - "github.com/favonia/cloudflare-ddns/internal/message" "github.com/favonia/cloudflare-ddns/internal/pp" ) -// DescribeAll calls [Monitor.Describe] for each monitor in the group with the callback. -func DescribeAll(callback func(service, params string), ms []Monitor) { - for _, m := range ms { - m.Describe(callback) - } -} +type monitors []BasicMonitor -// SuccessAll calls [Monitor.Success] for each monitor in the group. -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) { - ok = false - } - } - return ok -} +var _ Monitor = monitors{} -// StartAll calls [Monitor.Start] for each monitor in ms. -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) { - ok = false +// NewComposed creates a new composed monitor. +func NewComposed(mons ...BasicMonitor) monitors { + ms := make([]BasicMonitor, 0, len(mons)) + for _, m := range mons { + if m == nil { + continue + } + if list, composed := m.(monitors); composed { + ms = append(ms, list...) + } else { + ms = append(ms, m) } } - return ok + return monitors(ms) } -// FailureAll calls [Monitor.Failure] for each monitor in ms. -func FailureAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, message string) bool { - ok := true +// Describe calls [Monitor.Describe] for each monitor in the group with the callback. +func (ms monitors) Describe(yield func(name string, params string) bool) { for _, m := range ms { - if !m.Failure(ctx, ppfmt, message) { - ok = false + for name, params := range m.Describe { + if !yield(name, params) { + return + } } } - return ok } -// LogAll calls [Monitor.Log] for each monitor in ms. -func LogAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, message string) bool { +// Ping calls [Monitor.Ping] for each monitor in the group. +func (ms monitors) Ping(ctx context.Context, ppfmt pp.PP, message Message) bool { ok := true for _, m := range ms { - if !m.Log(ctx, ppfmt, message) { - ok = false - } + ok = ok && m.Ping(ctx, ppfmt, message) } return ok } -// ExitStatusAll calls [Monitor.ExitStatus] for each monitor in ms. -func ExitStatusAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, code int, message string) bool { +// Start calls [Monitor.Start] for each monitor in ms. +func (ms monitors) Start(ctx context.Context, ppfmt pp.PP, message string) bool { ok := true for _, m := range ms { - if !m.ExitStatus(ctx, ppfmt, code, message) { - ok = false + if em, extended := m.(Monitor); extended { + ok = ok && em.Start(ctx, ppfmt, message) } } return ok } -// PingMessageAll calls [SendMessage] for each monitor in ms. -func PingMessageAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, msg message.MonitorMessage) bool { +// Exit calls [Monitor.Exit] for each monitor in ms. +func (ms monitors) Exit(ctx context.Context, ppfmt pp.PP, message string) bool { ok := true for _, m := range ms { - if !PingMessage(ctx, ppfmt, m, msg) { - ok = false + if em, extended := m.(Monitor); extended { + ok = ok && em.Exit(ctx, ppfmt, message) } } return ok } -// LogMessageAll calls [SendMessage] for each monitor in ms. -func LogMessageAll(ctx context.Context, ppfmt pp.PP, ms []Monitor, msg message.MonitorMessage) bool { +// Log calls [Monitor.Log] for each monitor in the group. +func (ms monitors) Log(ctx context.Context, ppfmt pp.PP, msg Message) bool { ok := true for _, m := range ms { - if !LogMessage(ctx, ppfmt, m, msg) { - ok = false + if em, extended := m.(Monitor); extended { + ok = ok && em.Log(ctx, ppfmt, msg) + } else if !msg.OK { + ok = ok && m.Ping(ctx, ppfmt, msg) } } return ok diff --git a/internal/monitor/composite_test.go b/internal/monitor/composite_test.go index a35069d5..5432c607 100644 --- a/internal/monitor/composite_test.go +++ b/internal/monitor/composite_test.go @@ -3,113 +3,113 @@ package monitor_test import ( "context" "fmt" - "strings" "testing" + "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" - "github.com/favonia/cloudflare-ddns/internal/message" "github.com/favonia/cloudflare-ddns/internal/mocks" "github.com/favonia/cloudflare-ddns/internal/monitor" ) -func TestDescribeAll(t *testing.T) { +func TestComposedDescribe(t *testing.T) { t.Parallel() - ms := make([]monitor.Monitor, 0, 5) - mockCtrl := gomock.NewController(t) - for range 5 { + ms1 := make([]monitor.BasicMonitor, 0, 5) + for range 3 { m := mocks.NewMockMonitor(mockCtrl) - m.EXPECT().Describe(gomock.Any()) - ms = append(ms, m) + m.EXPECT().Describe(gomock.Any()).DoAndReturn( + func(yield func(string, string) bool) { + yield("name", "params") + }, + ) + ms1 = append(ms1, m) } - - callback := func(_service, _params string) { /* the callback content is not relevant here. */ } - monitor.DescribeAll(callback, ms) -} - -func TestSuccessAll(t *testing.T) { - t.Parallel() - - ms := make([]monitor.Monitor, 0, 5) - - mockCtrl := gomock.NewController(t) - mockPP := mocks.NewMockPP(mockCtrl) - - message := "aloha" - - for range 5 { + ms2 := make([]monitor.BasicMonitor, 0, 5) + for range 2 { m := mocks.NewMockMonitor(mockCtrl) - m.EXPECT().Success(context.Background(), mockPP, message) - ms = append(ms, m) + ms2 = append(ms2, m) } + c := monitor.NewComposed(monitor.NewComposed(ms1...), monitor.NewComposed(ms2...)) - monitor.SuccessAll(context.Background(), mockPP, ms, message) -} - -func TestStartAll(t *testing.T) { - t.Parallel() - - ms := make([]monitor.Monitor, 0, 5) - - mockCtrl := gomock.NewController(t) - mockPP := mocks.NewMockPP(mockCtrl) - - message := "你好" - - for range 5 { - m := mocks.NewMockMonitor(mockCtrl) - m.EXPECT().Start(context.Background(), mockPP, message) - ms = append(ms, m) + count := 0 + for range c.Describe { + count++ + if count >= 3 { + break + } } - - monitor.StartAll(context.Background(), mockPP, ms, message) + require.Equal(t, 3, count) } -func TestFailureAll(t *testing.T) { +func TestComposedPing(t *testing.T) { t.Parallel() - ms := make([]monitor.Monitor, 0, 5) + for name1, tc1 := range map[string]struct { + lines []string + }{ + "nil": {nil}, + "empty": {[]string{}}, + "one": {[]string{"hi"}}, + "two": {[]string{"hi", "hey"}}, + } { + for name2, tc2 := range map[string]struct { + ok bool + }{ + "ok": {true}, + "not-ok": {false}, + } { + t.Run(fmt.Sprintf("%s/%s", name1, name2), func(t *testing.T) { + t.Parallel() - mockCtrl := gomock.NewController(t) - mockPP := mocks.NewMockPP(mockCtrl) + ms := make([]monitor.BasicMonitor, 0, 5) + mockCtrl := gomock.NewController(t) + mockPP := mocks.NewMockPP(mockCtrl) - message := "secret" + msg := monitor.Message{ + OK: tc2.ok, + Lines: tc1.lines, + } - for range 5 { - m := mocks.NewMockMonitor(mockCtrl) - m.EXPECT().Failure(context.Background(), mockPP, message) - ms = append(ms, m) - } + for range 5 { + m := mocks.NewMockMonitor(mockCtrl) + m.EXPECT().Ping(context.Background(), mockPP, msg).Return(true) + ms = append(ms, m) + } - monitor.FailureAll(context.Background(), mockPP, ms, message) + ok := monitor.NewComposed(ms...).Ping(context.Background(), mockPP, msg) + require.True(t, ok) + }) + } + } } -func TestLogAll(t *testing.T) { +func TestComposedStart(t *testing.T) { t.Parallel() - ms := make([]monitor.Monitor, 0, 5) + ms := make([]monitor.BasicMonitor, 0, 5) mockCtrl := gomock.NewController(t) mockPP := mocks.NewMockPP(mockCtrl) - message := "forest" + message := "你好" for range 5 { m := mocks.NewMockMonitor(mockCtrl) - m.EXPECT().Log(context.Background(), mockPP, message) + m.EXPECT().Start(context.Background(), mockPP, message).Return(true) ms = append(ms, m) } - monitor.LogAll(context.Background(), mockPP, ms, message) + ok := monitor.NewComposed(ms...).Start(context.Background(), mockPP, message) + require.True(t, ok) } -func TestExitStatusAll(t *testing.T) { +func TestComposedExit(t *testing.T) { t.Parallel() - ms := make([]monitor.Monitor, 0, 5) + ms := make([]monitor.BasicMonitor, 0, 5) mockCtrl := gomock.NewController(t) mockPP := mocks.NewMockPP(mockCtrl) @@ -118,14 +118,15 @@ func TestExitStatusAll(t *testing.T) { for range 5 { m := mocks.NewMockMonitor(mockCtrl) - m.EXPECT().ExitStatus(context.Background(), mockPP, 42, message) + m.EXPECT().Exit(context.Background(), mockPP, message).Return(true) ms = append(ms, m) } - monitor.ExitStatusAll(context.Background(), mockPP, ms, 42, message) + ok := monitor.NewComposed(ms...).Exit(context.Background(), mockPP, message) + require.True(t, ok) } -func TestPingMessageAll(t *testing.T) { +func TestComposedLog(t *testing.T) { t.Parallel() for name1, tc1 := range map[string]struct { @@ -136,84 +137,39 @@ func TestPingMessageAll(t *testing.T) { "one": {[]string{"hi"}}, "two": {[]string{"hi", "hey"}}, } { - monitorMessage := strings.Join(tc1.lines, "\n") - for name2, tc2 := range map[string]struct { ok bool }{ - "ok": {true}, - "notok": {false}, + "ok": {true}, + "not-ok": {false}, } { t.Run(fmt.Sprintf("%s/%s", name1, name2), func(t *testing.T) { t.Parallel() - ms := make([]monitor.Monitor, 0, 5) + ms := make([]monitor.BasicMonitor, 0, 5) mockCtrl := gomock.NewController(t) mockPP := mocks.NewMockPP(mockCtrl) - for range 5 { - m := mocks.NewMockMonitor(mockCtrl) - if tc2.ok { - m.EXPECT().Success(context.Background(), mockPP, monitorMessage) - } else { - m.EXPECT().Failure(context.Background(), mockPP, monitorMessage) - } - ms = append(ms, m) - } - - msg := message.MonitorMessage{ + msg := monitor.Message{ OK: tc2.ok, Lines: tc1.lines, } - monitor.PingMessageAll(context.Background(), mockPP, ms, msg) - }) - } - } -} - -func TestLogMessageAll(t *testing.T) { - t.Parallel() - for name1, tc1 := range map[string]struct { - lines []string - }{ - "nil": {nil}, - "empty": {[]string{}}, - "one": {[]string{"hi"}}, - "two": {[]string{"hi", "hey"}}, - } { - monitorMessage := strings.Join(tc1.lines, "\n") - - for name2, tc2 := range map[string]struct { - ok bool - }{ - "ok": {true}, - "notok": {false}, - } { - t.Run(fmt.Sprintf("%s/%s", name1, name2), func(t *testing.T) { - t.Parallel() - - ms := make([]monitor.Monitor, 0, 5) - mockCtrl := gomock.NewController(t) - mockPP := mocks.NewMockPP(mockCtrl) - - for range 5 { + for range 3 { m := mocks.NewMockMonitor(mockCtrl) - switch { - case tc2.ok && len(monitorMessage) > 0: - m.EXPECT().Log(context.Background(), mockPP, monitorMessage) - case tc2.ok: - default: // !tc.ok - m.EXPECT().Failure(context.Background(), mockPP, monitorMessage) + m.EXPECT().Log(context.Background(), mockPP, msg).Return(true) + ms = append(ms, m) + } + for range 2 { + m := mocks.NewMockBasicMonitor(mockCtrl) + if !tc2.ok { + m.EXPECT().Ping(context.Background(), mockPP, msg).Return(true) } ms = append(ms, m) } - msg := message.MonitorMessage{ - OK: tc2.ok, - Lines: tc1.lines, - } - monitor.LogMessageAll(context.Background(), mockPP, ms, msg) + ok := monitor.NewComposed(ms...).Log(context.Background(), mockPP, msg) + require.True(t, ok) }) } } diff --git a/internal/monitor/healthchecks.go b/internal/monitor/healthchecks.go index ec4a4745..5bc9fe02 100644 --- a/internal/monitor/healthchecks.go +++ b/internal/monitor/healthchecks.go @@ -2,7 +2,6 @@ package monitor import ( "context" - "fmt" "io" "net/http" "net/url" @@ -69,8 +68,8 @@ func NewHealthchecks(ppfmt pp.PP, rawURL string) (Healthchecks, bool) { } // Describe calls the callback with the service name "Healthchecks". -func (h Healthchecks) Describe(callback func(service, params string)) { - callback("Healthchecks", "(URL redacted)") +func (h Healthchecks) Describe(yield func(service, params string) bool) { + yield("Healthchecks", "(URL redacted)") } /* @@ -153,9 +152,13 @@ func (h Healthchecks) ping(ctx context.Context, ppfmt pp.PP, endpoint string, me return true } -// Success pings the root endpoint. -func (h Healthchecks) Success(ctx context.Context, ppfmt pp.PP, message string) bool { - return h.ping(ctx, ppfmt, "", message) +// Ping formats and pings with a [Message]. +func (h Healthchecks) Ping(ctx context.Context, ppfmt pp.PP, msg Message) bool { + if msg.OK { + return h.ping(ctx, ppfmt, "", msg.Format()) + } else { + return h.ping(ctx, ppfmt, "/fail", msg.Format()) + } } // Start pings the /start endpoint. @@ -163,22 +166,19 @@ func (h Healthchecks) Start(ctx context.Context, ppfmt pp.PP, message string) bo return h.ping(ctx, ppfmt, "/start", message) } -// Failure pings the /fail endpoint. -func (h Healthchecks) Failure(ctx context.Context, ppfmt pp.PP, message string) bool { - return h.ping(ctx, ppfmt, "/fail", message) -} - -// Log pings the /log endpoint. -func (h Healthchecks) Log(ctx context.Context, ppfmt pp.PP, message string) bool { - return h.ping(ctx, ppfmt, "/log", message) +// Exit pings the /0 endpoint. +func (h Healthchecks) Exit(ctx context.Context, ppfmt pp.PP, message string) bool { + return h.ping(ctx, ppfmt, "/0", message) } -// ExitStatus pings the /number endpoint where number is the exit status. -func (h Healthchecks) ExitStatus(ctx context.Context, ppfmt pp.PP, code int, message string) bool { - if code < 0 || code > 255 { - ppfmt.Noticef(pp.EmojiImpossible, "Exit code (%d) not within the range 0-255", code) - return false +// Log formats and logs a [Message]. +func (h Healthchecks) Log(ctx context.Context, ppfmt pp.PP, msg Message) bool { + switch { + case !msg.OK: + return h.ping(ctx, ppfmt, "/fail", msg.Format()) + case !msg.IsEmpty(): + return h.ping(ctx, ppfmt, "/log", msg.Format()) + default: + return true } - - return h.ping(ctx, ppfmt, fmt.Sprintf("/%d", code), message) } diff --git a/internal/monitor/healthchecks_test.go b/internal/monitor/healthchecks_test.go index 8ec77616..ad175817 100644 --- a/internal/monitor/healthchecks_test.go +++ b/internal/monitor/healthchecks_test.go @@ -34,7 +34,7 @@ func TestNewHealthchecks(t *testing.T) { require.True(t, ok) } -func TestHealthchecksNewHealthchecksFail1(t *testing.T) { +func TestNewHealthchecksFail1(t *testing.T) { t.Parallel() mockCtrl := gomock.NewController(t) @@ -47,7 +47,7 @@ func TestHealthchecksNewHealthchecksFail1(t *testing.T) { require.False(t, ok) } -func TestHealthchecksNewHealthchecksFail2(t *testing.T) { +func TestNewHealthchecksFail2(t *testing.T) { t.Parallel() mockCtrl := gomock.NewController(t) @@ -60,7 +60,7 @@ func TestHealthchecksNewHealthchecksFail2(t *testing.T) { require.False(t, ok) } -func TestHealthchecksNewHealthchecksFail3(t *testing.T) { +func TestNewHealthchecksFail3(t *testing.T) { t.Parallel() mockCtrl := gomock.NewController(t) @@ -70,16 +70,21 @@ func TestHealthchecksNewHealthchecksFail3(t *testing.T) { require.False(t, ok) } -func TestHealthchecksDescripbe(t *testing.T) { +func TestHealthchecksDescribe(t *testing.T) { t.Parallel() mockCtrl := gomock.NewController(t) mockPP := mocks.NewMockPP(mockCtrl) + m, ok := monitor.NewHealthchecks(mockPP, "https://user:pass@host/path") require.True(t, ok) - m.Describe(func(service, _params string) { - require.Equal(t, "Healthchecks", service) - }) + + count := 0 + for name := range m.Describe { + count++ + require.Equal(t, "Healthchecks", name) + } + require.Equal(t, 1, count) } //nolint:funlen @@ -100,18 +105,18 @@ func TestHealthchecksEndPoints(t *testing.T) { message string actions []action defaultAction action - pinged bool + pinged int ok bool prepareMockPP func(*mocks.MockPP) }{ "success": { func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.Success(context.Background(), ppfmt, "hello") + return m.Ping(context.Background(), ppfmt, monitor.NewMessagef(true, "hello")) }, "/", "hello", []action{ActionAbort, ActionAbort, ActionOK}, - ActionAbort, - true, true, + ActionAbort, 1, + true, func(m *mocks.MockPP) { gomock.InOrder( m.EXPECT().Noticef(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS"), @@ -119,14 +124,14 @@ func TestHealthchecksEndPoints(t *testing.T) { ) }, }, - "success/notok": { + "success/not-ok": { func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.Success(context.Background(), ppfmt, "aloha") + return m.Ping(context.Background(), ppfmt, monitor.NewMessagef(true, "aloha")) }, "/", "aloha", []action{ActionAbort, ActionAbort, ActionNotOK}, - ActionAbort, - false, false, + ActionAbort, 0, + false, func(m *mocks.MockPP) { gomock.InOrder( m.EXPECT().Noticef(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS"), @@ -136,12 +141,11 @@ func TestHealthchecksEndPoints(t *testing.T) { }, "success/abort/all": { func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.Success(context.Background(), ppfmt, "stop now") + return m.Ping(context.Background(), ppfmt, monitor.NewMessagef(true, "stop now")) }, "/", "stop now", - nil, - ActionAbort, - false, false, + nil, ActionAbort, 0, + false, func(m *mocks.MockPP) { gomock.InOrder( m.EXPECT().Noticef(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS"), @@ -149,29 +153,14 @@ func TestHealthchecksEndPoints(t *testing.T) { ) }, }, - "start": { - func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.Start(context.Background(), ppfmt, "starting now!") - }, - "/start", "starting now!", - []action{ActionAbort, ActionAbort, ActionOK}, - ActionAbort, - true, true, - func(m *mocks.MockPP) { - gomock.InOrder( - m.EXPECT().Noticef(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS"), - m.EXPECT().Infof(pp.EmojiPing, "Pinged the %s endpoint of Healthchecks", `"/start"`), - ) - }, - }, "failure": { func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.Failure(context.Background(), ppfmt, "something's wrong") + return m.Ping(context.Background(), ppfmt, monitor.NewMessagef(false, "something's wrong")) }, "/fail", "something's wrong", []action{ActionAbort, ActionAbort, ActionOK}, - ActionAbort, - true, true, + ActionAbort, 1, + true, func(m *mocks.MockPP) { gomock.InOrder( m.EXPECT().Noticef(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS"), @@ -179,29 +168,29 @@ func TestHealthchecksEndPoints(t *testing.T) { ) }, }, - "log": { + "start": { func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.Log(context.Background(), ppfmt, "message") + return m.Start(context.Background(), ppfmt, "starting now!") }, - "/log", "message", + "/start", "starting now!", []action{ActionAbort, ActionAbort, ActionOK}, - ActionAbort, - true, true, + ActionAbort, 1, + true, func(m *mocks.MockPP) { gomock.InOrder( m.EXPECT().Noticef(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS"), - m.EXPECT().Infof(pp.EmojiPing, "Pinged the %s endpoint of Healthchecks", `"/log"`), + m.EXPECT().Infof(pp.EmojiPing, "Pinged the %s endpoint of Healthchecks", `"/start"`), ) }, }, - "exitstatus/0": { + "exits": { func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.ExitStatus(context.Background(), ppfmt, 0, "bye!") + return m.Exit(context.Background(), ppfmt, "bye!") }, "/0", "bye!", []action{ActionAbort, ActionAbort, ActionOK}, - ActionAbort, - true, true, + ActionAbort, 1, + true, func(m *mocks.MockPP) { gomock.InOrder( m.EXPECT().Noticef(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS"), @@ -209,35 +198,48 @@ func TestHealthchecksEndPoints(t *testing.T) { ) }, }, - "exitstatus/1": { + "log": { func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.ExitStatus(context.Background(), ppfmt, 1, "did exit(1)") + return m.Log(context.Background(), ppfmt, monitor.NewMessagef(true, "message")) }, - "/1", "did exit(1)", + "/log", "message", []action{ActionAbort, ActionAbort, ActionOK}, - ActionAbort, - true, true, + ActionAbort, 1, + true, func(m *mocks.MockPP) { gomock.InOrder( m.EXPECT().Noticef(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS"), - m.EXPECT().Infof(pp.EmojiPing, "Pinged the %s endpoint of Healthchecks", `"/1"`), + m.EXPECT().Infof(pp.EmojiPing, "Pinged the %s endpoint of Healthchecks", `"/log"`), ) }, }, - "exitstatus/-1": { + "log/not-ok": { func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.ExitStatus(context.Background(), ppfmt, -1, "feeling negative") + return m.Log(context.Background(), ppfmt, monitor.NewMessagef(false, "oops!")) }, - "", "feeling negative", - nil, ActionAbort, - false, false, + "/fail", "oops!", + []action{ActionOK}, + ActionAbort, 1, + true, func(m *mocks.MockPP) { gomock.InOrder( m.EXPECT().Noticef(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS"), - m.EXPECT().Noticef(pp.EmojiImpossible, "Exit code (%d) not within the range 0-255", -1), + m.EXPECT().Infof(pp.EmojiPing, "Pinged the %s endpoint of Healthchecks", `"/fail"`), ) }, }, + "log/empty": { + func(ppfmt pp.PP, m monitor.Monitor) bool { + return m.Log(context.Background(), ppfmt, monitor.NewMessage()) + }, + "/log", "message", + []action{}, + ActionAbort, 0, + true, + func(m *mocks.MockPP) { + m.EXPECT().Noticef(pp.EmojiUserWarning, "The Healthchecks URL (redacted) uses HTTP; please consider using HTTPS") + }, + }, } { t.Run(name, func(t *testing.T) { t.Parallel() @@ -248,7 +250,7 @@ func TestHealthchecksEndPoints(t *testing.T) { } visited := 0 - pinged := false + pinged := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !assert.Equal(t, http.MethodPost, r.Method) || !assert.Equal(t, tc.url, r.URL.EscapedPath()) { @@ -267,7 +269,7 @@ func TestHealthchecksEndPoints(t *testing.T) { } switch action { case ActionOK: - pinged = true + pinged++ if _, err := io.WriteString(w, "OK"); !assert.NoError(t, err) { panic(http.ErrAbortHandler) } diff --git a/internal/monitor/message.go b/internal/monitor/message.go new file mode 100644 index 00000000..4717d989 --- /dev/null +++ b/internal/monitor/message.go @@ -0,0 +1,45 @@ +package monitor + +import ( + "fmt" + "slices" + "strings" +) + +// Message holds the messages and success/failure status for monitors. +type Message struct { + OK bool + Lines []string +} + +// NewMessage creates a new empty Message. +func NewMessage() Message { return Message{OK: true, Lines: nil} } + +// NewMessagef creates a new Message containing one formatted line. +func NewMessagef(ok bool, format string, args ...any) Message { + return Message{OK: ok, Lines: []string{fmt.Sprintf(format, args...)}} +} + +// Format turns the message into a single string. +func (m Message) Format() string { return strings.Join(m.Lines, "\n") } + +// MergeMessages keeps only the ones with highest severity. +func MergeMessages(msgs ...Message) Message { + var ( + OK = true + Lines = map[bool][][]string{} + ) + + for _, msg := range msgs { + OK = OK && msg.OK + Lines[msg.OK] = append(Lines[msg.OK], msg.Lines) + } + + return Message{ + OK: OK, + Lines: slices.Concat(Lines[OK]...), + } +} + +// IsEmpty checks if the message is empty. +func (m Message) IsEmpty() bool { return len(m.Lines) == 0 } diff --git a/internal/monitor/uptimekuma.go b/internal/monitor/uptimekuma.go index c18edc04..511ac619 100644 --- a/internal/monitor/uptimekuma.go +++ b/internal/monitor/uptimekuma.go @@ -30,7 +30,7 @@ type UptimeKuma struct { Timeout time.Duration } -var _ Monitor = UptimeKuma{} //nolint:exhaustruct +var _ BasicMonitor = UptimeKuma{} //nolint:exhaustruct const ( // UptimeKumaDefaultTimeout is the default timeout for a UptimeKuma ping. @@ -100,8 +100,8 @@ func NewUptimeKuma(ppfmt pp.PP, rawURL string) (UptimeKuma, bool) { } // Describe calls the callback with the service name "Uptime Kuma". -func (h UptimeKuma) Describe(callback func(service, params string)) { - callback("Uptime Kuma", "(URL redacted)") +func (h UptimeKuma) Describe(yield func(name, params string) bool) { + yield("Uptime Kuma", "(URL redacted)") } // UptimeKumaResponse is for parsing the response from Uptime Kuma. @@ -155,40 +155,23 @@ func (h UptimeKuma) ping(ctx context.Context, ppfmt pp.PP, param UptimeKumaReque return true } -// Success pings the server with status=up. Messages are ignored and "OK" is used instead. -// The reason is that Uptime Kuma seems to show only the first success message -// and it could be misleading if an outdated message stays in the UI. -func (h UptimeKuma) Success(ctx context.Context, ppfmt pp.PP, _message string) bool { - return h.ping(ctx, ppfmt, UptimeKumaRequest{Status: "up", Msg: "OK", Ping: ""}) -} - -// Start does nothing. -func (h UptimeKuma) Start(_ctx context.Context, _ppfmt pp.PP, _message string) bool { - return true -} +// Ping pings the server with status=up/down depending on Message.OK. +func (h UptimeKuma) Ping(ctx context.Context, ppfmt pp.PP, msg Message) bool { + if msg.OK { + // Pings the server with status=up. Messages are ignored and "OK" is used instead. + // The reason is that Uptime Kuma seems to show only the first success message + // and it could be misleading if an outdated message stays in the UI. + return h.ping(ctx, ppfmt, UptimeKumaRequest{Status: "up", Msg: "OK", Ping: ""}) + } -// Failure pings the server with status=down. -func (h UptimeKuma) Failure(ctx context.Context, ppfmt pp.PP, message string) bool { - if message == "" { + formatted := msg.Format() + if formatted == "" { // If we do not send a non-empty message to Uptime Kuma, it seems to // either keep the previous message (even if it was for success) or // assume the message is "OK". Either is bad. // // We can send a non-empty message to overwrite it. - message = "Failing" - } - return h.ping(ctx, ppfmt, UptimeKumaRequest{Status: "down", Msg: message, Ping: ""}) -} - -// Log does nothing. -func (h UptimeKuma) Log(_ctx context.Context, _ppfmt pp.PP, _message string) bool { - return true -} - -// ExitStatus with non-zero triggers [Failure]. Otherwise, it does nothing. -func (h UptimeKuma) ExitStatus(ctx context.Context, ppfmt pp.PP, code int, message string) bool { - if code != 0 { - return h.Failure(ctx, ppfmt, message) + formatted = "Failing" } - return true + return h.ping(ctx, ppfmt, UptimeKumaRequest{Status: "down", Msg: formatted, Ping: ""}) } diff --git a/internal/monitor/uptimekuma_test.go b/internal/monitor/uptimekuma_test.go index dd50fb71..71d42934 100644 --- a/internal/monitor/uptimekuma_test.go +++ b/internal/monitor/uptimekuma_test.go @@ -74,16 +74,21 @@ func TestNewUptimeKuma(t *testing.T) { } } -func TestUptimeKumaDescripbe(t *testing.T) { +func TestUptimeKumaDescribe(t *testing.T) { t.Parallel() mockCtrl := gomock.NewController(t) mockPP := mocks.NewMockPP(mockCtrl) + m, ok := monitor.NewUptimeKuma(mockPP, "https://user:pass@host/path") require.True(t, ok) - m.Describe(func(service, _params string) { - require.Equal(t, "Uptime Kuma", service) - }) + + count := 0 + for name := range m.Describe { + count++ + require.Equal(t, "Uptime Kuma", name) + } + require.Equal(t, 1, count) } //nolint:funlen @@ -107,7 +112,7 @@ func TestUptimeKumaEndPoints(t *testing.T) { } for name, tc := range map[string]struct { - endpoint func(pp.PP, monitor.Monitor) bool + endpoint func(pp.PP, monitor.BasicMonitor) bool url string status string msg string @@ -119,8 +124,8 @@ func TestUptimeKumaEndPoints(t *testing.T) { prepareMockPP func(*mocks.MockPP) }{ "success": { - func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.Success(context.Background(), ppfmt, "hello") + func(ppfmt pp.PP, m monitor.BasicMonitor) bool { + return m.Ping(context.Background(), ppfmt, monitor.NewMessagef(true, "hello")) }, "/", "up", "OK", "", []action{ActionOK}, @@ -129,8 +134,8 @@ func TestUptimeKumaEndPoints(t *testing.T) { successPP, }, "success/not-ok": { - func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.Success(context.Background(), ppfmt, "aloha") + func(ppfmt pp.PP, m monitor.BasicMonitor) bool { + return m.Ping(context.Background(), ppfmt, monitor.NewMessagef(true, "aloha")) }, "/", "up", "OK", "", []action{ActionNotOK}, @@ -144,8 +149,8 @@ func TestUptimeKumaEndPoints(t *testing.T) { }, }, "success/garbage-response": { - func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.Success(context.Background(), ppfmt, "aloha") + func(ppfmt pp.PP, m monitor.BasicMonitor) bool { + return m.Ping(context.Background(), ppfmt, monitor.NewMessagef(true, "aloha")) }, "/", "up", "OK", "", []action{ActionGarbage}, @@ -159,8 +164,8 @@ func TestUptimeKumaEndPoints(t *testing.T) { }, }, "success/abort/all": { - func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.Success(context.Background(), ppfmt, "stop now") + func(ppfmt pp.PP, m monitor.BasicMonitor) bool { + return m.Ping(context.Background(), ppfmt, monitor.NewMessagef(true, "stop now")) }, "/", "up", "OK", "", nil, ActionAbort, false, @@ -172,21 +177,9 @@ func TestUptimeKumaEndPoints(t *testing.T) { ) }, }, - "start": { - func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.Start(context.Background(), ppfmt, "starting now!") - }, - "/", "", "", "", - []action{}, - ActionAbort, false, - true, - func(m *mocks.MockPP) { - m.EXPECT().Noticef(pp.EmojiUserWarning, httpUnsafeMsg) - }, - }, "failure": { - func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.Failure(context.Background(), ppfmt, "something's wrong") + func(ppfmt pp.PP, m monitor.BasicMonitor) bool { + return m.Ping(context.Background(), ppfmt, monitor.NewMessagef(false, "something's wrong")) }, "/", "down", "something's wrong", "", []action{ActionOK}, @@ -195,8 +188,8 @@ func TestUptimeKumaEndPoints(t *testing.T) { successPP, }, "failure/empty": { - func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.Failure(context.Background(), ppfmt, "") + func(ppfmt pp.PP, m monitor.BasicMonitor) bool { + return m.Ping(context.Background(), ppfmt, monitor.NewMessagef(false, "")) }, "/", "down", "Failing", "", []action{ActionOK}, @@ -204,50 +197,6 @@ func TestUptimeKumaEndPoints(t *testing.T) { true, successPP, }, - "log": { - func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.Log(context.Background(), ppfmt, "message") - }, - "/", "", "", "", - []action{}, - ActionAbort, false, - true, - func(m *mocks.MockPP) { - m.EXPECT().Noticef(pp.EmojiUserWarning, httpUnsafeMsg) - }, - }, - "exitstatus/0": { - func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.ExitStatus(context.Background(), ppfmt, 0, "bye!") - }, - "/", "", "", "", - []action{}, - ActionAbort, false, - true, - func(m *mocks.MockPP) { - m.EXPECT().Noticef(pp.EmojiUserWarning, httpUnsafeMsg) - }, - }, - "exitstatus/1": { - func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.ExitStatus(context.Background(), ppfmt, 1, "did exit(1)") - }, - "/", "down", "did exit(1)", "", - []action{ActionOK}, - ActionAbort, true, - true, - successPP, - }, - "exitstatus/-1": { - func(ppfmt pp.PP, m monitor.Monitor) bool { - return m.ExitStatus(context.Background(), ppfmt, -1, "feeling negative") - }, - "/", "down", "feeling negative", "", - []action{ActionOK}, - ActionAbort, true, - true, - successPP, - }, } { t.Run(name, func(t *testing.T) { t.Parallel() diff --git a/internal/notifier/base.go b/internal/notifier/base.go index 3be1339b..4b78301c 100644 --- a/internal/notifier/base.go +++ b/internal/notifier/base.go @@ -4,7 +4,6 @@ package notifier import ( "context" - "github.com/favonia/cloudflare-ddns/internal/message" "github.com/favonia/cloudflare-ddns/internal/pp" ) @@ -13,16 +12,8 @@ import ( // Notifier is an abstract service for push notifications. type Notifier interface { // Describe a notifier in a human-readable format by calling callback with service names and params. - Describe(callback func(service, params string)) + Describe(yield func(name, params string) bool) // Send out a message. - Send(ctx context.Context, ppfmt pp.PP, msg string) bool -} - -// SendMessage formats and sends a [message.Message]. -func SendMessage(ctx context.Context, ppfmt pp.PP, n Notifier, msg message.NotifierMessage) bool { - if len(msg) == 0 { - return true - } - return n.Send(ctx, ppfmt, msg.Format()) + Send(ctx context.Context, ppfmt pp.PP, msg Message) bool } diff --git a/internal/notifier/composite.go b/internal/notifier/composite.go deleted file mode 100644 index 8ed97e52..00000000 --- a/internal/notifier/composite.go +++ /dev/null @@ -1,37 +0,0 @@ -package notifier - -import ( - "context" - - "github.com/favonia/cloudflare-ddns/internal/message" - "github.com/favonia/cloudflare-ddns/internal/pp" -) - -// DescribeAll calls [Notifier.Describe] for each monitor in the group with the callback. -func DescribeAll(callback func(service, params string), ns []Notifier) { - for _, n := range ns { - n.Describe(callback) - } -} - -// SendAll calls [Notifier.Send] for each monitor in the group. -func SendAll(ctx context.Context, ppfmt pp.PP, ns []Notifier, message string) bool { - ok := true - for _, n := range ns { - if !n.Send(ctx, ppfmt, message) { - ok = false - } - } - return ok -} - -// SendMessageAll calls [SendMessage] for each monitor in the group. -func SendMessageAll(ctx context.Context, ppfmt pp.PP, ns []Notifier, msg message.NotifierMessage) bool { - ok := true - for _, n := range ns { - if !SendMessage(ctx, ppfmt, n, msg) { - ok = false - } - } - return ok -} diff --git a/internal/notifier/composite_test.go b/internal/notifier/composite_test.go deleted file mode 100644 index 40cb87a0..00000000 --- a/internal/notifier/composite_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package notifier_test - -import ( - "context" - "strings" - "testing" - - "go.uber.org/mock/gomock" - - "github.com/favonia/cloudflare-ddns/internal/message" - "github.com/favonia/cloudflare-ddns/internal/mocks" - "github.com/favonia/cloudflare-ddns/internal/notifier" -) - -func TestDescribeAll(t *testing.T) { - t.Parallel() - - ms := make([]notifier.Notifier, 0, 5) - - mockCtrl := gomock.NewController(t) - - for range 5 { - m := mocks.NewMockNotifier(mockCtrl) - m.EXPECT().Describe(gomock.Any()) - ms = append(ms, m) - } - - callback := func(_service, _params string) { /* the callback content is not relevant here. */ } - notifier.DescribeAll(callback, ms) -} - -func TestSendAll(t *testing.T) { - t.Parallel() - - ms := make([]notifier.Notifier, 0, 5) - - mockCtrl := gomock.NewController(t) - mockPP := mocks.NewMockPP(mockCtrl) - - message := "aloha" - - for range 5 { - m := mocks.NewMockNotifier(mockCtrl) - m.EXPECT().Send(context.Background(), mockPP, message) - ms = append(ms, m) - } - - notifier.SendAll(context.Background(), mockPP, ms, message) -} - -func TestSendMessageAll(t *testing.T) { - t.Parallel() - - for name1, tc1 := range map[string]struct { - notifierMessages []string - }{ - "nil": {nil}, - "empty": {[]string{}}, - "one": {[]string{"hi"}}, - "two": {[]string{"hi", "hey"}}, - } { - notifierMessage := strings.Join(tc1.notifierMessages, " ") - - t.Run(name1, func(t *testing.T) { - t.Parallel() - - ns := make([]notifier.Notifier, 0, 5) - mockCtrl := gomock.NewController(t) - mockPP := mocks.NewMockPP(mockCtrl) - - for range 5 { - n := mocks.NewMockNotifier(mockCtrl) - if len(tc1.notifierMessages) > 0 { - n.EXPECT().Send(context.Background(), mockPP, notifierMessage) - } - ns = append(ns, n) - } - - msg := message.NotifierMessage(tc1.notifierMessages) - notifier.SendMessageAll(context.Background(), mockPP, ns, msg) - }) - } -} diff --git a/internal/notifier/message.go b/internal/notifier/message.go new file mode 100644 index 00000000..e250bf1a --- /dev/null +++ b/internal/notifier/message.go @@ -0,0 +1,29 @@ +package notifier + +import ( + "fmt" + "slices" + "strings" +) + +// Message holds the messages and success/failure status for notifiers. +type Message []string + +// NewMessage creates a new empty Message. +func NewMessage() Message { return nil } + +// NewMessagef creates a new MonitorMessage containing one formatted string. +func NewMessagef(format string, args ...any) Message { + return Message{fmt.Sprintf(format, args...)} +} + +// MergeMessages keeps only the ones with highest severity. +func MergeMessages(msgs ...Message) Message { + return slices.Concat[Message, string](msgs...) +} + +// Format turns the message into a single string. +func (m Message) Format() string { return strings.Join(m, " ") } + +// IsEmpty checks if the message is empty. +func (m Message) IsEmpty() bool { return len(m) == 0 } diff --git a/internal/notifier/shoutrrr.go b/internal/notifier/shoutrrr.go index fe5debf6..d806ef47 100644 --- a/internal/notifier/shoutrrr.go +++ b/internal/notifier/shoutrrr.go @@ -2,12 +2,13 @@ package notifier import ( "context" - "strconv" "time" "github.com/containrrr/shoutrrr" "github.com/containrrr/shoutrrr/pkg/router" "github.com/containrrr/shoutrrr/pkg/types" + "golang.org/x/text/cases" + "golang.org/x/text/language" "github.com/favonia/cloudflare-ddns/internal/pp" ) @@ -18,7 +19,7 @@ type Shoutrrr struct { Router *router.ServiceRouter // The services - ServiceNames []string + ServiceDescriptions []string } var _ Notifier = Shoutrrr{} //nolint:exhaustruct @@ -28,6 +29,40 @@ const ( ShoutrrrDefaultTimeout = 10 * time.Second ) +// DescribeShoutrrrService gives a human-readable description for a service. +func DescribeShoutrrrService(ppfmt pp.PP, proto string) string { + name, known := map[string]string{ + "bark": "Bark", + "discord": "Discord", + "smtp": "Email", + "gotify": "Gotify", + "googlechat": "Google Chat", + "ifttt": "IFTTT", + "join": "Join", + "mattermost": "Mattermost", + "matrix": "Matrix", + "ntfy": "Ntfy", + "opsgenie": "OpsGenie", + "pushbullet": "Pushbullet", + "pushover": "Pushover", + "rocketchat": "Rocketchat", + "slack": "Slack", + "teams": "Teams", + "telegram": "Telegram", + "zulip": "Zulip Chat", + "generic": "Generic", + }[proto] + + if known { + return name + } else { + ppfmt.Noticef(pp.EmojiImpossible, + "Unknown shoutrrr service name %q; please report it at %s", + name, pp.IssueReportingURL) + return cases.Title(language.English).String(proto) + } +} + // NewShoutrrr creates a new shoutrrr notifier. func NewShoutrrr(ppfmt pp.PP, rawURLs []string) (Shoutrrr, bool) { r, err := shoutrrr.CreateSender(rawURLs...) @@ -38,25 +73,31 @@ func NewShoutrrr(ppfmt pp.PP, rawURLs []string) (Shoutrrr, bool) { r.Timeout = ShoutrrrDefaultTimeout - serviceNames := make([]string, 0, len(rawURLs)) + serviceDescriptions := make([]string, 0, len(rawURLs)) for _, u := range rawURLs { s, _, _ := r.ExtractServiceName(u) - serviceNames = append(serviceNames, s) + serviceDescriptions = append(serviceDescriptions, DescribeShoutrrrService(ppfmt, s)) } - return Shoutrrr{Router: r, ServiceNames: serviceNames}, true + return Shoutrrr{Router: r, ServiceDescriptions: serviceDescriptions}, true } // Describe calls callback on each registered notification service. -func (s Shoutrrr) Describe(callback func(service, params string)) { - for _, n := range s.ServiceNames { - callback(n, "(URL redacted)") +func (s Shoutrrr) Describe(yield func(string, string) bool) { + for _, n := range s.ServiceDescriptions { + if !yield(n, "(URL redacted)") { + return + } } } // Send sents the message msg. -func (s Shoutrrr) Send(_ context.Context, ppfmt pp.PP, msg string) bool { - errs := s.Router.Send(msg, &types.Params{}) +func (s Shoutrrr) Send(_ context.Context, ppfmt pp.PP, msg Message) bool { + if msg.IsEmpty() { + return true + } + + errs := s.Router.Send(msg.Format(), &types.Params{}) allOK := true for _, err := range errs { if err != nil { @@ -65,7 +106,9 @@ func (s Shoutrrr) Send(_ context.Context, ppfmt pp.PP, msg string) bool { } } if allOK { - ppfmt.Infof(pp.EmojiNotify, "Notified %s via shoutrrr", pp.EnglishJoinMap(strconv.Quote, s.ServiceNames)) + ppfmt.Infof(pp.EmojiNotify, + "Notified %s via shoutrrr", + pp.EnglishJoin(s.ServiceDescriptions)) } return allOK } diff --git a/internal/notifier/shoutrrr_test.go b/internal/notifier/shoutrrr_test.go index 5b306f4a..fb59f1b4 100644 --- a/internal/notifier/shoutrrr_test.go +++ b/internal/notifier/shoutrrr_test.go @@ -16,16 +16,66 @@ import ( "github.com/favonia/cloudflare-ddns/internal/pp" ) -func TestShoutrrrDescripbe(t *testing.T) { +func TestDescribeShoutrrrService(t *testing.T) { + t.Parallel() + + for name, tc := range map[string]struct { + input string + output string + prepareMocks func(*mocks.MockPP) + }{ + "ifttt": {"ifttt", "IFTTT", nil}, + "zulip": {"zulip", "Zulip Chat", nil}, + "empty": { + "", "", + func(ppfmt *mocks.MockPP) { + ppfmt.EXPECT().Noticef(pp.EmojiImpossible, + "Unknown shoutrrr service name %q; please report it at %s", + "", pp.IssueReportingURL) + }, + }, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + mockCtrl := gomock.NewController(t) + mockPP := mocks.NewMockPP(mockCtrl) + if tc.prepareMocks != nil { + tc.prepareMocks(mockPP) + } + + output := notifier.DescribeShoutrrrService(mockPP, tc.input) + require.Equal(t, tc.output, output) + }) + } +} + +func TestShoutrrrDescribe(t *testing.T) { t.Parallel() mockCtrl := gomock.NewController(t) mockPP := mocks.NewMockPP(mockCtrl) - m, ok := notifier.NewShoutrrr(mockPP, []string{"generic://localhost/"}) - require.True(t, ok) - m.Describe(func(service, _params string) { - require.Equal(t, "generic", service) + m, ok := notifier.NewShoutrrr(mockPP, []string{ + "generic://localhost/", + "gotify://host:80/path/tokentoken", + "ifttt://hey/?events=1", }) + require.True(t, ok) + + count := 0 +outer: + for name := range m.Describe { + count++ + switch count { + case 1: + require.Equal(t, "Generic", name) + case 2: + require.Equal(t, "Gotify", name) + break outer + default: + } + } + require.Equal(t, 2, count) } func TestShoutrrrSend(t *testing.T) { @@ -33,30 +83,37 @@ func TestShoutrrrSend(t *testing.T) { for name, tc := range map[string]struct { path string + pinged int service func(serverURL string) string - message string - pinged bool + message notifier.Message ok bool prepareMockPP func(*mocks.MockPP) }{ "success": { - "/greeting", + "/greeting", 1, func(serverURL string) string { return "generic+" + serverURL + "/greeting" }, - "hello", - true, true, + notifier.NewMessagef("hello"), + true, func(m *mocks.MockPP) { - m.EXPECT().Infof(pp.EmojiNotify, "Notified %s via shoutrrr", `"generic"`) + m.EXPECT().Infof(pp.EmojiNotify, "Notified %s via shoutrrr", "Generic") }, }, "ill-formed url": { - "", + "", 0, func(_serverURL string) string { return "generic+https://0.0.0.0" }, - "hello", - false, false, + notifier.NewMessagef("hello"), + false, func(m *mocks.MockPP) { m.EXPECT().Noticef(pp.EmojiError, "Failed to notify shoutrrr service(s): %v", gomock.Any()) }, }, + "empty": { + "/greeting", 0, + func(serverURL string) string { return "generic+" + serverURL + "/greeting" }, + notifier.NewMessage(), + true, + nil, + }, } { t.Run(name, func(t *testing.T) { t.Parallel() @@ -66,7 +123,7 @@ func TestShoutrrrSend(t *testing.T) { tc.prepareMockPP(mockPP) } - pinged := false + pinged := 0 server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { if !assert.Equal(t, http.MethodPost, r.Method) || !assert.Equal(t, tc.path, r.URL.EscapedPath()) { @@ -74,11 +131,11 @@ func TestShoutrrrSend(t *testing.T) { } if reqBody, err := io.ReadAll(r.Body); !assert.NoError(t, err) || - !assert.Equal(t, tc.message, string(reqBody)) { + !assert.Equal(t, tc.message.Format(), string(reqBody)) { panic(http.ErrAbortHandler) } - pinged = true + pinged++ })) s, ok := notifier.NewShoutrrr(mockPP, []string{tc.service(server.URL)}) diff --git a/internal/updater/message.go b/internal/updater/message.go index ec8db343..19cbbc0f 100644 --- a/internal/updater/message.go +++ b/internal/updater/message.go @@ -8,6 +8,8 @@ import ( "github.com/favonia/cloudflare-ddns/internal/domain" "github.com/favonia/cloudflare-ddns/internal/ipnet" "github.com/favonia/cloudflare-ddns/internal/message" + "github.com/favonia/cloudflare-ddns/internal/monitor" + "github.com/favonia/cloudflare-ddns/internal/notifier" "github.com/favonia/cloudflare-ddns/internal/pp" "github.com/favonia/cloudflare-ddns/internal/setter" ) @@ -28,20 +30,20 @@ func generateDetectMessage(ipNet ipnet.Type, ok bool) message.Message { return message.New() case !ok: return message.Message{ - MonitorMessage: message.MonitorMessage{ + MonitorMessage: monitor.Message{ OK: false, Lines: []string{fmt.Sprintf("Failed to detect %s address", ipNet.Describe())}, }, - NotifierMessage: message.NotifierMessage{ + NotifierMessage: notifier.Message{ fmt.Sprintf("Failed to detect the %s address.", ipNet.Describe()), }, } } } -func generateUpdateMonitorMessage(ipNet ipnet.Type, ip netip.Addr, s setterResponses) message.MonitorMessage { +func generateUpdateMonitorMessage(ipNet ipnet.Type, ip netip.Addr, s setterResponses) monitor.Message { if domains := s[setter.ResponseFailed]; len(domains) > 0 { - return message.MonitorMessage{ + return monitor.Message{ OK: false, Lines: []string{fmt.Sprintf( "Failed to set %s (%s) of %s", @@ -66,10 +68,10 @@ func generateUpdateMonitorMessage(ipNet ipnet.Type, ip netip.Addr, s setterRespo )) } - return message.MonitorMessage{OK: true, Lines: successLines} + return monitor.Message{OK: true, Lines: successLines} } -func generateUpdateNotifierMessage(ipNet ipnet.Type, ip netip.Addr, s setterResponses) message.NotifierMessage { +func generateUpdateNotifierMessage(ipNet ipnet.Type, ip netip.Addr, s setterResponses) notifier.Message { var fragments []string if domains := s[setter.ResponseFailed]; len(domains) > 0 { @@ -106,7 +108,7 @@ func generateUpdateNotifierMessage(ipNet ipnet.Type, ip netip.Addr, s setterResp return nil } else { fragments = append(fragments, ".") - return message.NotifierMessage{strings.Join(fragments, "")} + return notifier.Message{strings.Join(fragments, "")} } } @@ -117,9 +119,9 @@ func generateUpdateMessage(ipNet ipnet.Type, ip netip.Addr, s setterResponses) m } } -func generateDeleteMonitorMessage(ipNet ipnet.Type, s setterResponses) message.MonitorMessage { +func generateDeleteMonitorMessage(ipNet ipnet.Type, s setterResponses) monitor.Message { if domains := s[setter.ResponseFailed]; len(domains) > 0 { - return message.MonitorMessage{ + return monitor.Message{ OK: false, Lines: []string{fmt.Sprintf( "Failed to delete %s of %s", @@ -144,10 +146,10 @@ func generateDeleteMonitorMessage(ipNet ipnet.Type, s setterResponses) message.M )) } - return message.MonitorMessage{OK: true, Lines: successLines} + return monitor.Message{OK: true, Lines: successLines} } -func generateDeleteNotifierMessage(ipNet ipnet.Type, s setterResponses) message.NotifierMessage { +func generateDeleteNotifierMessage(ipNet ipnet.Type, s setterResponses) notifier.Message { var fragments []string if domains := s[setter.ResponseFailed]; len(domains) > 0 { @@ -184,7 +186,7 @@ func generateDeleteNotifierMessage(ipNet ipnet.Type, s setterResponses) message. return nil } else { fragments = append(fragments, ".") - return message.NotifierMessage{strings.Join(fragments, "")} + return notifier.Message{strings.Join(fragments, "")} } } diff --git a/internal/updater/message_waf.go b/internal/updater/message_waf.go index 31e92d54..1808137a 100644 --- a/internal/updater/message_waf.go +++ b/internal/updater/message_waf.go @@ -4,6 +4,8 @@ import ( "strings" "github.com/favonia/cloudflare-ddns/internal/message" + "github.com/favonia/cloudflare-ddns/internal/monitor" + "github.com/favonia/cloudflare-ddns/internal/notifier" "github.com/favonia/cloudflare-ddns/internal/pp" "github.com/favonia/cloudflare-ddns/internal/setter" ) @@ -18,9 +20,9 @@ func (s setterWAFListResponses) register(name string, code setter.ResponseCode) s[code] = append(s[code], name) } -func generateUpdateWAFListsMonitorMessage(s setterWAFListResponses) message.MonitorMessage { +func generateUpdateWAFListsMonitorMessage(s setterWAFListResponses) monitor.Message { if domains := s[setter.ResponseFailed]; len(domains) > 0 { - return message.MonitorMessage{ + return monitor.Message{ OK: false, Lines: []string{"Failed to set list(s) " + pp.Join(domains)}, } @@ -36,10 +38,10 @@ func generateUpdateWAFListsMonitorMessage(s setterWAFListResponses) message.Moni successLines = append(successLines, "Set list(s) "+pp.Join(domains)) } - return message.MonitorMessage{OK: true, Lines: successLines} + return monitor.Message{OK: true, Lines: successLines} } -func generateUpdateWAFListsNotifierMessage(s setterWAFListResponses) message.NotifierMessage { +func generateUpdateWAFListsNotifierMessage(s setterWAFListResponses) notifier.Message { var fragments []string if domains := s[setter.ResponseFailed]; len(domains) > 0 { @@ -66,7 +68,7 @@ func generateUpdateWAFListsNotifierMessage(s setterWAFListResponses) message.Not return nil } else { fragments = append(fragments, ".") - return message.NotifierMessage{strings.Join(fragments, "")} + return notifier.Message{strings.Join(fragments, "")} } } @@ -77,9 +79,9 @@ func generateUpdateWAFListsMessage(s setterWAFListResponses) message.Message { } } -func generateClearWAFListsMonitorMessage(s setterWAFListResponses) message.MonitorMessage { +func generateClearWAFListsMonitorMessage(s setterWAFListResponses) monitor.Message { if domains := s[setter.ResponseFailed]; len(domains) > 0 { - return message.MonitorMessage{ + return monitor.Message{ OK: false, Lines: []string{"Failed to clear list(s) " + pp.Join(domains)}, } @@ -95,10 +97,10 @@ func generateClearWAFListsMonitorMessage(s setterWAFListResponses) message.Monit successLines = append(successLines, "Cleared list(s) "+pp.Join(domains)) } - return message.MonitorMessage{OK: true, Lines: successLines} + return monitor.Message{OK: true, Lines: successLines} } -func generateClearWAFListsNotifierMessage(s setterWAFListResponses) message.NotifierMessage { +func generateClearWAFListsNotifierMessage(s setterWAFListResponses) notifier.Message { var fragments []string if domains := s[setter.ResponseFailed]; len(domains) > 0 { @@ -125,7 +127,7 @@ func generateClearWAFListsNotifierMessage(s setterWAFListResponses) message.Noti return nil } else { fragments = append(fragments, ".") - return message.NotifierMessage{strings.Join(fragments, "")} + return notifier.Message{strings.Join(fragments, "")} } } diff --git a/internal/updater/updater.go b/internal/updater/updater.go index 40e80d67..e02fb7ab 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -164,7 +164,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.MonitorMessage.OK { numValidIPs++ msgs = append(msgs, setIP(ctx, ppfmt, c, s, ipNet, ip)) } diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go index 37b5b45a..b56480ca 100644 --- a/internal/updater/updater_test.go +++ b/internal/updater/updater_test.go @@ -15,6 +15,8 @@ import ( "github.com/favonia/cloudflare-ddns/internal/ipnet" "github.com/favonia/cloudflare-ddns/internal/message" "github.com/favonia/cloudflare-ddns/internal/mocks" + "github.com/favonia/cloudflare-ddns/internal/monitor" + "github.com/favonia/cloudflare-ddns/internal/notifier" "github.com/favonia/cloudflare-ddns/internal/pp" "github.com/favonia/cloudflare-ddns/internal/provider" "github.com/favonia/cloudflare-ddns/internal/provider/protocol" @@ -177,11 +179,11 @@ func TestUpdateIPsMultiple(t *testing.T) { resp := updater.UpdateIPs(ctx, mockPP, conf, mockSetter) require.Equal(t, message.Message{ - MonitorMessage: message.MonitorMessage{ + MonitorMessage: monitor.Message{ OK: tc.ok, Lines: tc.monitorMessages, }, - NotifierMessage: message.NotifierMessage(tc.notifierMessages), + NotifierMessage: notifier.Message(tc.notifierMessages), }, resp) }) } @@ -278,11 +280,11 @@ func TestDeleteIPsMultiple(t *testing.T) { } resp := updater.DeleteIPs(ctx, mockPP, conf, mockSetter) require.Equal(t, message.Message{ - MonitorMessage: message.MonitorMessage{ + MonitorMessage: monitor.Message{ OK: tc.ok, Lines: tc.monitorMessages, }, - NotifierMessage: message.NotifierMessage(tc.notifierMessages), + NotifierMessage: notifier.Message(tc.notifierMessages), }, resp) }) } @@ -649,11 +651,11 @@ func TestUpdateIPs(t *testing.T) { } resp := updater.UpdateIPs(ctx, mockPP, conf, mockSetter) require.Equal(t, message.Message{ - MonitorMessage: message.MonitorMessage{ + MonitorMessage: monitor.Message{ OK: tc.ok, Lines: tc.monitorMessages, }, - NotifierMessage: message.NotifierMessage(tc.notifierMessages), + NotifierMessage: notifier.Message(tc.notifierMessages), }, resp) }) } @@ -874,11 +876,11 @@ func TestDeleteIPs(t *testing.T) { resp := updater.DeleteIPs(ctx, mockPP, conf, mockSetter) require.Equal(t, message.Message{ - MonitorMessage: message.MonitorMessage{ + MonitorMessage: monitor.Message{ OK: tc.ok, Lines: tc.monitorMessages, }, - NotifierMessage: message.NotifierMessage(tc.notifierMessages), + NotifierMessage: notifier.Message(tc.notifierMessages), }, resp) }) }