package notifications import ( "time" "github.com/containrrr/shoutrrr/pkg/types" "github.com/containrrr/watchtower/internal/actions/mocks" "github.com/containrrr/watchtower/internal/flags" s "github.com/containrrr/watchtower/pkg/session" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gbytes" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) var allButTrace = logrus.DebugLevel var legacyMockData = Data{ Entries: []*logrus.Entry{ { Level: logrus.InfoLevel, Message: "foo Bar", }, }, } var mockDataMultipleEntries = Data{ Entries: []*logrus.Entry{ { Level: logrus.InfoLevel, Message: "The situation is under control", }, { Level: logrus.WarnLevel, Message: "All the smoke might be covering up some problems", }, { Level: logrus.ErrorLevel, Message: "Turns out everything is on fire", }, }, } var mockDataAllFresh = Data{ Entries: []*logrus.Entry{}, Report: mocks.CreateMockProgressReport(s.FreshState), } func mockDataFromStates(states ...s.State) Data { hostname := "Mock" prefix := "" return Data{ Entries: legacyMockData.Entries, Report: mocks.CreateMockProgressReport(states...), StaticData: StaticData{ Title: GetTitle(hostname, prefix), Host: hostname, }, } } var _ = Describe("Shoutrrr", func() { var logBuffer *gbytes.Buffer BeforeEach(func() { logBuffer = gbytes.NewBuffer() logrus.SetOutput(logBuffer) logrus.SetLevel(logrus.TraceLevel) logrus.SetFormatter(&logrus.TextFormatter{ DisableColors: true, DisableTimestamp: true, }) }) When("passing a common template name", func() { It("should format using that template", func() { expected := ` updt1 (mock/updt1:latest): Updated `[1:] data := mockDataFromStates(s.UpdatedState) Expect(getTemplatedResult(`porcelain.v1.summary-no-log`, false, data)).To(Equal(expected)) }) }) When("adding a log hook", func() { When("it has not been added before", func() { It("should be added to the logrus hooks", func() { level := logrus.TraceLevel hooksBefore := len(logrus.StandardLogger().Hooks[level]) shoutrrr := createNotifier([]string{}, level, "", true, StaticData{}, false, time.Second) shoutrrr.AddLogHook() hooksAfter := len(logrus.StandardLogger().Hooks[level]) Expect(hooksAfter).To(BeNumerically(">", hooksBefore)) }) }) When("it is being added a second time", func() { It("should not be added to the logrus hooks", func() { level := logrus.TraceLevel shoutrrr := createNotifier([]string{}, level, "", true, StaticData{}, false, time.Second) shoutrrr.AddLogHook() hooksBefore := len(logrus.StandardLogger().Hooks[level]) shoutrrr.AddLogHook() hooksAfter := len(logrus.StandardLogger().Hooks[level]) Expect(hooksAfter).To(Equal(hooksBefore)) }) }) }) When("using legacy templates", func() { When("no custom template is provided", func() { It("should format the messages using the default template", func() { cmd := new(cobra.Command) flags.RegisterNotificationFlags(cmd) shoutrrr := createNotifier([]string{}, logrus.TraceLevel, "", true, StaticData{}, false, time.Second) entries := []*logrus.Entry{ { Message: "foo bar", }, } s, err := shoutrrr.buildMessage(Data{Entries: entries}) Expect(err).NotTo(HaveOccurred()) Expect(s).To(Equal("foo bar\n")) }) }) When("given a valid custom template", func() { It("should format the messages using the custom template", func() { tplString := `{{range .}}{{.Level}}: {{.Message}}{{println}}{{end}}` tpl, err := getShoutrrrTemplate(tplString, true) Expect(err).ToNot(HaveOccurred()) shoutrrr := &shoutrrrTypeNotifier{ template: tpl, legacyTemplate: true, } entries := []*logrus.Entry{ { Level: logrus.InfoLevel, Message: "foo bar", }, } s, err := shoutrrr.buildMessage(Data{Entries: entries}) Expect(err).NotTo(HaveOccurred()) Expect(s).To(Equal("info: foo bar\n")) }) }) Describe("the default template", func() { When("all containers are fresh", func() { It("should return an empty string", func() { Expect(getTemplatedResult(``, true, mockDataAllFresh)).To(Equal("")) }) }) }) When("given an invalid custom template", func() { It("should format the messages using the default template", func() { invNotif, err := createNotifierWithTemplate(`{{ intentionalSyntaxError`, true) Expect(err).To(HaveOccurred()) invMsg, err := invNotif.buildMessage(legacyMockData) Expect(err).NotTo(HaveOccurred()) defNotif, err := createNotifierWithTemplate(``, true) Expect(err).ToNot(HaveOccurred()) defMsg, err := defNotif.buildMessage(legacyMockData) Expect(err).ToNot(HaveOccurred()) Expect(invMsg).To(Equal(defMsg)) }) }) When("given a template that is using ToUpper function", func() { It("should return the text in UPPER CASE", func() { tplString := `{{range .}}{{ .Message | ToUpper }}{{end}}` Expect(getTemplatedResult(tplString, true, legacyMockData)).To(Equal("FOO BAR")) }) }) When("given a template that is using ToLower function", func() { It("should return the text in lower case", func() { tplString := `{{range .}}{{ .Message | ToLower }}{{end}}` Expect(getTemplatedResult(tplString, true, legacyMockData)).To(Equal("foo bar")) }) }) When("given a template that is using Title function", func() { It("should return the text in Title Case", func() { tplString := `{{range .}}{{ .Message | Title }}{{end}}` Expect(getTemplatedResult(tplString, true, legacyMockData)).To(Equal("Foo Bar")) }) }) }) When("using report templates", func() { When("no custom template is provided", func() { It("should format the messages using the default template", func() { expected := `4 Scanned, 2 Updated, 1 Failed - updt1 (mock/updt1:latest): 01d110000000 updated to d0a110000000 - updt2 (mock/updt2:latest): 01d120000000 updated to d0a120000000 - frsh1 (mock/frsh1:latest): Fresh - skip1 (mock/skip1:latest): Skipped: unpossible - fail1 (mock/fail1:latest): Failed: accidentally the whole container` data := mockDataFromStates(s.UpdatedState, s.FreshState, s.FailedState, s.SkippedState, s.UpdatedState) Expect(getTemplatedResult(``, false, data)).To(Equal(expected)) }) }) When("using a template referencing Title", func() { It("should contain the title in the output", func() { expected := `Watchtower updates on Mock` data := mockDataFromStates(s.UpdatedState) Expect(getTemplatedResult(`{{ .Title }}`, false, data)).To(Equal(expected)) }) }) When("using a template referencing Host", func() { It("should contain the hostname in the output", func() { expected := `Mock` data := mockDataFromStates(s.UpdatedState) Expect(getTemplatedResult(`{{ .Host }}`, false, data)).To(Equal(expected)) }) }) Describe("the default template", func() { When("all containers are fresh", func() { It("should return an empty string", func() { Expect(getTemplatedResult(``, false, mockDataAllFresh)).To(Equal("")) }) }) When("at least one container was updated", func() { It("should send a report", func() { expected := `1 Scanned, 1 Updated, 0 Failed - updt1 (mock/updt1:latest): 01d110000000 updated to d0a110000000` data := mockDataFromStates(s.UpdatedState) Expect(getTemplatedResult(``, false, data)).To(Equal(expected)) }) }) When("at least one container failed to update", func() { It("should send a report", func() { expected := `1 Scanned, 0 Updated, 1 Failed - fail1 (mock/fail1:latest): Failed: accidentally the whole container` data := mockDataFromStates(s.FailedState) Expect(getTemplatedResult(``, false, data)).To(Equal(expected)) }) }) When("the report is nil", func() { It("should return the logged entries", func() { expected := `The situation is under control All the smoke might be covering up some problems Turns out everything is on fire ` Expect(getTemplatedResult(``, false, mockDataMultipleEntries)).To(Equal(expected)) }) }) }) }) When("batching notifications", func() { When("no messages are queued", func() { It("should not send any notification", func() { shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{}, false, time.Duration(0)) shoutrrr.StartNotification() shoutrrr.SendNotification(nil) Consistently(logBuffer).ShouldNot(gbytes.Say(`Shoutrrr:`)) }) }) When("at least one message is queued", func() { It("should send a notification", func() { shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{}, false, time.Duration(0)) shoutrrr.AddLogHook() shoutrrr.StartNotification() logrus.Info("This log message is sponsored by ContainrrrVPN") shoutrrr.SendNotification(nil) Eventually(logBuffer).Should(gbytes.Say(`Shoutrrr: This log message is sponsored by ContainrrrVPN`)) }) }) }) When("the title data field is empty", func() { It("should not have set the title param", func() { shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{ Host: "test.host", Title: "", }, false, time.Second) _, found := shoutrrr.params.Title() Expect(found).ToNot(BeTrue()) }) }) When("sending notifications", func() { It("SlowNotificationNotSent", func() { _, blockingRouter := sendNotificationsWithBlockingRouter(true) Eventually(blockingRouter.sent).Should(Not(Receive())) }) It("SlowNotificationSent", func() { shoutrrr, blockingRouter := sendNotificationsWithBlockingRouter(true) blockingRouter.unlock <- true shoutrrr.Close() Eventually(blockingRouter.sent).Should(Receive(BeTrue())) }) }) }) type blockingRouter struct { unlock chan bool sent chan bool } func (b blockingRouter) Send(_ string, _ *types.Params) []error { <-b.unlock b.sent <- true return nil } func sendNotificationsWithBlockingRouter(legacy bool) (*shoutrrrTypeNotifier, *blockingRouter) { router := &blockingRouter{ unlock: make(chan bool, 1), sent: make(chan bool, 1), } tpl, err := getShoutrrrTemplate("", legacy) Expect(err).NotTo(HaveOccurred()) shoutrrr := &shoutrrrTypeNotifier{ template: tpl, messages: make(chan string, 1), done: make(chan bool), Router: router, legacyTemplate: legacy, params: &types.Params{}, delay: time.Duration(0), } entry := &logrus.Entry{ Message: "foo bar", } go sendNotifications(shoutrrr) shoutrrr.StartNotification() _ = shoutrrr.Fire(entry) shoutrrr.SendNotification(nil) return shoutrrr, router } func createNotifierWithTemplate(tplString string, legacy bool) (*shoutrrrTypeNotifier, error) { tpl, err := getShoutrrrTemplate(tplString, legacy) return &shoutrrrTypeNotifier{ template: tpl, legacyTemplate: legacy, }, err } func getTemplatedResult(tplString string, legacy bool, data Data) (msg string) { notifier, err := createNotifierWithTemplate(tplString, legacy) ExpectWithOffset(1, err).NotTo(HaveOccurred()) msg, err = notifier.buildMessage(data) ExpectWithOffset(1, err).NotTo(HaveOccurred()) return msg }