notifier on success after failure - service downtime readable

pull/78/head
Hunter Long 2018-09-20 02:46:51 -07:00
parent 8dbf573667
commit e3c572da75
22 changed files with 308 additions and 37 deletions

View File

@ -1,4 +1,4 @@
VERSION=0.64 VERSION=0.65
BINARY_NAME=statup BINARY_NAME=statup
GOPATH:=$(GOPATH) GOPATH:=$(GOPATH)
GOCMD=go GOCMD=go

View File

@ -58,6 +58,7 @@ type Notification struct {
Queue []interface{} `gorm:"-" json:"-"` Queue []interface{} `gorm:"-" json:"-"`
Running chan bool `gorm:"-" json:"-"` Running chan bool `gorm:"-" json:"-"`
CanTest bool `gorm:"-" json:"-"` CanTest bool `gorm:"-" json:"-"`
Online bool `gorm:"-" json:"-"`
} }
type NotificationForm struct { type NotificationForm struct {

View File

@ -232,9 +232,7 @@ func TestRunAllQueueAndStop(t *testing.T) {
assert.Equal(t, 16, len(example.Queue)) assert.Equal(t, 16, len(example.Queue))
go Queue(example) go Queue(example)
assert.Equal(t, 16, len(example.Queue)) assert.Equal(t, 16, len(example.Queue))
time.Sleep(13 * time.Second) time.Sleep(15 * time.Second)
assert.Equal(t, 6, len(example.Queue))
time.Sleep(1 * time.Second)
assert.Equal(t, 6, len(example.Queue)) assert.Equal(t, 6, len(example.Queue))
example.close() example.close()
assert.False(t, example.IsRunning()) assert.False(t, example.IsRunning())

View File

@ -1,3 +1,20 @@
/*
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> and the project contributors
*
* https://github.com/hunterlong/statup
*
* The licenses for most software and other practical works are designed
* to take away your freedom to share and change the works. By contrast,
* the GNU General Public License is intended to guarantee your freedom to
* share and change all versions of a program--to make sure it remains free
* software for all its users.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
context('Setup Process', () => { context('Setup Process', () => {
// it('should go to setup Statup with Postgres', () => { // it('should go to setup Statup with Postgres', () => {
@ -45,4 +62,4 @@ context('Setup Process', () => {
cy.get('.card').should('have.length', 5) cy.get('.card').should('have.length', 5)
}) })
}) })

View File

@ -1,3 +1,20 @@
/*
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> and the project contributors
*
* https://github.com/hunterlong/statup
*
* The licenses for most software and other practical works are designed
* to take away your freedom to share and change the works. By contrast,
* the GNU General Public License is intended to guarantee your freedom to
* share and change all versions of a program--to make sure it remains free
* software for all its users.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
context('Asset Tests', () => { context('Asset Tests', () => {
beforeEach(function () { beforeEach(function () {
@ -36,4 +53,4 @@ context('Asset Tests', () => {
cy.request('http://localhost:8080/css/base.css').its('body').should('contain', 'BODY') cy.request('http://localhost:8080/css/base.css').its('body').should('contain', 'BODY')
}) })
}); });

View File

@ -1,3 +1,20 @@
/*
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> and the project contributors
*
* https://github.com/hunterlong/statup
*
* The licenses for most software and other practical works are designed
* to take away your freedom to share and change the works. By contrast,
* the GNU General Public License is intended to guarantee your freedom to
* share and change all versions of a program--to make sure it remains free
* software for all its users.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
context('Dashboard Tests', () => { context('Dashboard Tests', () => {
beforeEach(function() { beforeEach(function() {
@ -21,4 +38,4 @@ context('Dashboard Tests', () => {
cy.get('.col-12 > :nth-child(1)').should('contain', 'Statup') cy.get('.col-12 > :nth-child(1)').should('contain', 'Statup')
}) })
}); });

View File

@ -1,3 +1,20 @@
/*
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> and the project contributors
*
* https://github.com/hunterlong/statup
*
* The licenses for most software and other practical works are designed
* to take away your freedom to share and change the works. By contrast,
* the GNU General Public License is intended to guarantee your freedom to
* share and change all versions of a program--to make sure it remains free
* software for all its users.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
context('Service Tests', () => { context('Service Tests', () => {
beforeEach(function () { beforeEach(function () {

View File

@ -1,3 +1,20 @@
/*
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> and the project contributors
*
* https://github.com/hunterlong/statup
*
* The licenses for most software and other practical works are designed
* to take away your freedom to share and change the works. By contrast,
* the GNU General Public License is intended to guarantee your freedom to
* share and change all versions of a program--to make sure it remains free
* software for all its users.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
context('Settings Forms', () => { context('Settings Forms', () => {
beforeEach(function() { beforeEach(function() {
@ -28,4 +45,4 @@ context('Settings Forms', () => {
// cy.get('.header-desc').should('contain', 'This is an awesome page') // cy.get('.header-desc').should('contain', 'This is an awesome page')
// }) // })
}); });

View File

@ -1,3 +1,20 @@
/*
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> and the project contributors
*
* https://github.com/hunterlong/statup
*
* The licenses for most software and other practical works are designed
* to take away your freedom to share and change the works. By contrast,
* the GNU General Public License is intended to guarantee your freedom to
* share and change all versions of a program--to make sure it remains free
* software for all its users.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
context('User Testing', () => { context('User Testing', () => {
beforeEach(function () { beforeEach(function () {
@ -49,4 +66,4 @@ context('User Testing', () => {
cy.get('tr').should('have.length', 2) cy.get('tr').should('have.length', 2)
}) })
}); });

View File

@ -1,3 +1,20 @@
/*
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> and the project contributors
*
* https://github.com/hunterlong/statup
*
* The licenses for most software and other practical works are designed
* to take away your freedom to share and change the works. By contrast,
* the GNU General Public License is intended to guarantee your freedom to
* share and change all versions of a program--to make sure it remains free
* software for all its users.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// *********************************************************** // ***********************************************************
// This example plugins/index.js can be used to load plugins // This example plugins/index.js can be used to load plugins
// //

View File

@ -1,3 +1,20 @@
/*
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> and the project contributors
*
* https://github.com/hunterlong/statup
*
* The licenses for most software and other practical works are designed
* to take away your freedom to share and change the works. By contrast,
* the GNU General Public License is intended to guarantee your freedom to
* share and change all versions of a program--to make sure it remains free
* software for all its users.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// *********************************************** // ***********************************************
// This example commands.js shows you how to // This example commands.js shows you how to
// create various custom commands and overwrite // create various custom commands and overwrite

View File

@ -1,3 +1,20 @@
/*
* Statup
* Copyright (C) 2018. Hunter Long and the project contributors
* Written by Hunter Long <info@socialeck.com> and the project contributors
*
* https://github.com/hunterlong/statup
*
* The licenses for most software and other practical works are designed
* to take away your freedom to share and change the works. By contrast,
* the GNU General Public License is intended to guarantee your freedom to
* share and change all versions of a program--to make sure it remains free
* software for all its users.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// *********************************************************** // ***********************************************************
// This example support/index.js is processed and // This example support/index.js is processed and
// loaded automatically before your test files. // loaded automatically before your test files.

View File

@ -32,32 +32,27 @@ type Service struct {
} }
func renderServiceChartHandler(w http.ResponseWriter, r *http.Request) { func renderServiceChartHandler(w http.ResponseWriter, r *http.Request) {
if !IsAuthenticated(r) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
vars := mux.Vars(r) vars := mux.Vars(r)
fields := parseGet(r) fields := parseGet(r)
w.Header().Set("Content-Type", "text/javascript")
w.Header().Set("Cache-Control", "max-age=30")
startField := fields.Get("start") startField := fields.Get("start")
endField := fields.Get("end") endField := fields.Get("end")
var start time.Time var start time.Time
var end time.Time var end time.Time
if startField == "" { if startField == "" {
start = time.Now().Add(-24 * time.Hour) start = time.Now().Add(-24 * time.Hour).UTC()
} else { } else {
start = time.Unix(utils.StringInt(startField), 0) start = time.Unix(utils.StringInt(startField), 0).UTC()
} }
if endField == "" { if endField == "" {
end = time.Now() end = time.Now().UTC()
} else { } else {
end = time.Unix(utils.StringInt(endField), 0) end = time.Unix(utils.StringInt(endField), 0).UTC()
} }
service := core.SelectService(utils.StringInt(vars["id"])) service := core.SelectService(utils.StringInt(vars["id"]))
w.Header().Set("Content-Type", "text/javascript")
w.Header().Set("Cache-Control", "max-age=60")
data := core.GraphDataRaw(service, start, end).ToString() data := core.GraphDataRaw(service, start, end).ToString()
out := struct { out := struct {

View File

@ -54,8 +54,8 @@ func init() {
// Send will send a HTTP Post to the Discord API. It accepts type: []byte // Send will send a HTTP Post to the Discord API. It accepts type: []byte
func (u *Discord) Send(msg interface{}) error { func (u *Discord) Send(msg interface{}) error {
message := msg.([]byte) message := msg.(string)
req, _ := http.NewRequest("POST", discorder.GetValue("host"), bytes.NewBuffer(message)) req, _ := http.NewRequest("POST", discorder.GetValue("host"), bytes.NewBuffer([]byte(message)))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
client := &http.Client{} client := &http.Client{}
resp, err := client.Do(req) resp, err := client.Do(req)
@ -73,11 +73,16 @@ func (u *Discord) Select() *notifier.Notification {
func (u *Discord) OnFailure(s *types.Service, f *types.Failure) { func (u *Discord) OnFailure(s *types.Service, f *types.Failure) {
msg := fmt.Sprintf(`{"content": "Your service '%v' is currently failing! Reason: %v"}`, s.Name, f.Issue) msg := fmt.Sprintf(`{"content": "Your service '%v' is currently failing! Reason: %v"}`, s.Name, f.Issue)
u.AddQueue(msg) u.AddQueue(msg)
u.Online = false
} }
// OnSuccess will trigger successful service // OnSuccess will trigger successful service
func (u *Discord) OnSuccess(s *types.Service) { func (u *Discord) OnSuccess(s *types.Service) {
if !u.Online {
msg := fmt.Sprintf(`{"content": "Your service '%v' is back online!"}`, s.Name)
u.AddQueue(msg)
}
u.Online = true
} }
// OnSave triggers when this notifier has been saved // OnSave triggers when this notifier has been saved

View File

@ -60,8 +60,31 @@ func TestDiscordNotifier(t *testing.T) {
assert.True(t, ok) assert.True(t, ok)
}) })
t.Run("Discord OnFailure", func(t *testing.T) {
discorder.OnFailure(TestService, TestFailure)
assert.Len(t, discorder.Queue, 1)
})
t.Run("Discord Check Offline", func(t *testing.T) {
assert.False(t, discorder.Online)
})
t.Run("Discord OnSuccess", func(t *testing.T) {
discorder.OnSuccess(TestService)
assert.Len(t, discorder.Queue, 2)
})
t.Run("Discord Check Back Online", func(t *testing.T) {
assert.True(t, discorder.Online)
})
t.Run("Discord OnSuccess Again", func(t *testing.T) {
discorder.OnSuccess(TestService)
assert.Len(t, discorder.Queue, 2)
})
t.Run("Discord Send", func(t *testing.T) { t.Run("Discord Send", func(t *testing.T) {
err := discorder.Send([]byte(discordMessage)) err := discorder.Send(discordMessage)
assert.Nil(t, err) assert.Nil(t, err)
}) })

File diff suppressed because one or more lines are too long

View File

@ -97,11 +97,34 @@ func TestEmailNotifier(t *testing.T) {
assert.True(t, ok) assert.True(t, ok)
}) })
t.Run("Emailer Test Source", func(t *testing.T) { t.Run("Email Test Source", func(t *testing.T) {
emailSource(testEmail) emailSource(testEmail)
assert.NotEmpty(t, testEmail.Source) assert.NotEmpty(t, testEmail.Source)
}) })
t.Run("Email OnFailure", func(t *testing.T) {
emailer.OnFailure(TestService, TestFailure)
assert.Len(t, emailer.Queue, 1)
})
t.Run("Email Check Offline", func(t *testing.T) {
assert.False(t, emailer.Online)
})
t.Run("Email OnSuccess", func(t *testing.T) {
emailer.OnSuccess(TestService)
assert.Len(t, emailer.Queue, 2)
})
t.Run("Email Check Back Online", func(t *testing.T) {
assert.True(t, emailer.Online)
})
t.Run("Email OnSuccess Again", func(t *testing.T) {
emailer.OnSuccess(TestService)
assert.Len(t, emailer.Queue, 2)
})
t.Run("Email Send", func(t *testing.T) { t.Run("Email Send", func(t *testing.T) {
err := emailer.Send(testEmail) err := emailer.Send(testEmail)
assert.Nil(t, err) assert.Nil(t, err)

View File

@ -80,11 +80,16 @@ func (u *LineNotify) Select() *notifier.Notification {
func (u *LineNotify) OnFailure(s *types.Service, f *types.Failure) { func (u *LineNotify) OnFailure(s *types.Service, f *types.Failure) {
msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name) msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name)
u.AddQueue(msg) u.AddQueue(msg)
u.Online = false
} }
// OnSuccess will trigger successful service // OnSuccess will trigger successful service
func (u *LineNotify) OnSuccess(s *types.Service) { func (u *LineNotify) OnSuccess(s *types.Service) {
if !u.Online {
msg := fmt.Sprintf("Your service '%v' is back online!", s.Name)
u.AddQueue(msg)
}
u.Online = true
} }
// OnSave triggers when this notifier has been saved // OnSave triggers when this notifier has been saved

View File

@ -27,11 +27,10 @@ import (
) )
const ( const (
SLACK_ID = 2
SLACK_METHOD = "slack" SLACK_METHOD = "slack"
FAILING_TEMPLATE = `{ "attachments": [ { "fallback": "Service {{.Service.Name}} - is currently failing", "text": "<{{.Service.Domain}}|{{.Service.Name}}> - Your Statup service '{{.Service.Name}}' has just received a Failure notification with a HTTP Status code of {{.Service.LastStatusCode}}.", "fields": [ { "title": "Expected", "value": "{{.Service.Expected}}", "short": true }, { "title": "Status Code", "value": "{{.Service.LastStatusCode}}", "short": true } ], "color": "#FF0000", "thumb_url": "https://statup.io", "footer": "Statup", "footer_icon": "https://img.cjx.io/statuplogo32.png" } ] }` FAILING_TEMPLATE = `{ "attachments": [ { "fallback": "Service {{.Service.Name}} - is currently failing", "text": "<{{.Service.Domain}}|{{.Service.Name}}> - Your Statup service '{{.Service.Name}}' has just received a Failure notification with a HTTP Status code of {{.Service.LastStatusCode}}.", "fields": [ { "title": "Expected", "value": "{{.Service.Expected}}", "short": true }, { "title": "Status Code", "value": "{{.Service.LastStatusCode}}", "short": true } ], "color": "#FF0000", "thumb_url": "https://statup.io", "footer": "Statup", "footer_icon": "https://img.cjx.io/statuplogo32.png" } ] }`
SUCCESS_TEMPLATE = `{ "attachments": [ { "fallback": "Service {{.Service.Name}} - is now back online", "text": "<{{.Service.Domain}}|{{.Service.Name}}> - Your Statup service '{{.Service.Name}}' has just received a Failure notification.", "fields": [ { "title": "Issue", "value": "Awesome Project", "short": true }, { "title": "Status Code", "value": "{{.Service.LastStatusCode}}", "short": true } ], "color": "#00FF00", "thumb_url": "https://statup.io", "footer": "Statup", "footer_icon": "https://img.cjx.io/statuplogo32.png" } ] }` SUCCESS_TEMPLATE = `{ "attachments": [ { "fallback": "Service {{.Service.Name}} - is now back online", "text": "<{{.Service.Domain}}|{{.Service.Name}}> - Your Statup service '{{.Service.Name}}' has just received a Failure notification.", "fields": [ { "title": "Issue", "value": "Awesome Project", "short": true }, { "title": "Status Code", "value": "{{.Service.LastStatusCode}}", "short": true } ], "color": "#00FF00", "thumb_url": "https://statup.io", "footer": "Statup", "footer_icon": "https://img.cjx.io/statuplogo32.png" } ] }`
TEST_TEMPLATE = `{"text":"{{.}}"}` SLACK_TEXT = `{"text":"{{.}}"}`
) )
type Slack struct { type Slack struct {
@ -91,7 +90,6 @@ func (u *Slack) Send(msg interface{}) error {
} }
defer res.Body.Close() defer res.Body.Close()
//contents, _ := ioutil.ReadAll(res.Body) //contents, _ := ioutil.ReadAll(res.Body)
//fmt.Println(string(contents))
return nil return nil
} }
@ -102,7 +100,7 @@ func (u *Slack) Select() *notifier.Notification {
func (u *Slack) OnTest(n notifier.Notification) (bool, error) { func (u *Slack) OnTest(n notifier.Notification) (bool, error) {
utils.Log(1, "Slack notifier loaded") utils.Log(1, "Slack notifier loaded")
msg := fmt.Sprintf("You're Statup Slack Notifier is working correctly!") msg := fmt.Sprintf("You're Statup Slack Notifier is working correctly!")
err := parseSlackMessage(TEST_TEMPLATE, msg) err := parseSlackMessage(SLACK_TEXT, msg)
return true, err return true, err
} }
@ -110,15 +108,24 @@ func (u *Slack) OnTest(n notifier.Notification) (bool, error) {
func (u *Slack) OnFailure(s *types.Service, f *types.Failure) { func (u *Slack) OnFailure(s *types.Service, f *types.Failure) {
message := SlackMessage{ message := SlackMessage{
Service: s, Service: s,
Template: FAILURE, Template: FAILING_TEMPLATE,
Time: time.Now().Unix(), Time: time.Now().Unix(),
} }
parseSlackMessage(FAILING_TEMPLATE, message) parseSlackMessage(FAILING_TEMPLATE, message)
u.Online = false
} }
// OnSuccess will trigger successful service // OnSuccess will trigger successful service
func (u *Slack) OnSuccess(s *types.Service) { func (u *Slack) OnSuccess(s *types.Service) {
if !u.Online {
message := SlackMessage{
Service: s,
Template: SUCCESS_TEMPLATE,
Time: time.Now().Unix(),
}
parseSlackMessage(SUCCESS_TEMPLATE, message)
}
u.Online = true
} }
// OnSave triggers when this notifier has been saved // OnSave triggers when this notifier has been saved

View File

@ -40,6 +40,8 @@ func init() {
func TestSlackNotifier(t *testing.T) { func TestSlackNotifier(t *testing.T) {
t.Parallel() t.Parallel()
SLACK_URL = os.Getenv("SLACK_URL")
slacker.Host = SLACK_URL
if SLACK_URL == "" { if SLACK_URL == "" {
t.Log("Slack notifier testing skipped, missing SLACK_URL environment variable") t.Log("Slack notifier testing skipped, missing SLACK_URL environment variable")
t.SkipNow() t.SkipNow()
@ -60,7 +62,7 @@ func TestSlackNotifier(t *testing.T) {
}) })
t.Run("Slack parse message", func(t *testing.T) { t.Run("Slack parse message", func(t *testing.T) {
err := parseSlackMessage("this is a test message!", slackTestMessage) err := parseSlackMessage(SLACK_TEXT, "this is a test!")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, len(slacker.Queue)) assert.Equal(t, 1, len(slacker.Queue))
}) })
@ -71,14 +73,38 @@ func TestSlackNotifier(t *testing.T) {
assert.True(t, ok) assert.True(t, ok)
}) })
t.Run("Slack OnFailure", func(t *testing.T) {
slacker.OnFailure(TestService, TestFailure)
assert.Len(t, slacker.Queue, 2)
})
t.Run("Slack Check Offline", func(t *testing.T) {
assert.False(t, slacker.Online)
})
t.Run("Slack OnSuccess", func(t *testing.T) {
slacker.OnSuccess(TestService)
assert.Len(t, slacker.Queue, 3)
})
t.Run("Slack Check Back Online", func(t *testing.T) {
assert.True(t, slacker.Online)
})
t.Run("Slack OnSuccess Again", func(t *testing.T) {
slacker.OnSuccess(TestService)
assert.Len(t, slacker.Queue, 3)
})
t.Run("Slack Send", func(t *testing.T) { t.Run("Slack Send", func(t *testing.T) {
err := slacker.Send(slackMessage) err := slacker.Send(slackMessage)
assert.Nil(t, err) assert.Nil(t, err)
assert.Len(t, slacker.Queue, 3)
}) })
t.Run("Slack Queue", func(t *testing.T) { t.Run("Slack Queue", func(t *testing.T) {
go notifier.Queue(slacker) go notifier.Queue(slacker)
time.Sleep(1 * time.Second) time.Sleep(2 * time.Second)
assert.Equal(t, SLACK_URL, slacker.Host) assert.Equal(t, SLACK_URL, slacker.Host)
assert.Equal(t, 0, len(slacker.Queue)) assert.Equal(t, 0, len(slacker.Queue))
}) })

View File

@ -28,7 +28,7 @@ var (
TWILIO_SECRET = os.Getenv("TWILIO_SECRET") TWILIO_SECRET = os.Getenv("TWILIO_SECRET")
TWILIO_FROM = os.Getenv("TWILIO_FROM") TWILIO_FROM = os.Getenv("TWILIO_FROM")
TWILIO_TO = os.Getenv("TWILIO_TO") TWILIO_TO = os.Getenv("TWILIO_TO")
twilioMessage = "The twilioNotifier notifier on Statup has been tested!" twilioMessage = "The Twilio notifier on Statup has been tested!"
) )
func init() { func init() {
@ -70,6 +70,29 @@ func TestTwilioNotifier(t *testing.T) {
assert.True(t, ok) assert.True(t, ok)
}) })
t.Run("Twilio OnFailure", func(t *testing.T) {
twilioNotifier.OnFailure(TestService, TestFailure)
assert.Len(t, twilioNotifier.Queue, 2)
})
t.Run("Twilio Check Offline", func(t *testing.T) {
assert.False(t, twilioNotifier.Online)
})
t.Run("Twilio OnSuccess", func(t *testing.T) {
twilioNotifier.OnSuccess(TestService)
assert.Len(t, twilioNotifier.Queue, 3)
})
t.Run("Twilio Check Back Online", func(t *testing.T) {
assert.True(t, twilioNotifier.Online)
})
t.Run("Twilio OnSuccess Again", func(t *testing.T) {
twilioNotifier.OnSuccess(TestService)
assert.Len(t, twilioNotifier.Queue, 3)
})
t.Run("Twilio Send", func(t *testing.T) { t.Run("Twilio Send", func(t *testing.T) {
err := twilioNotifier.Send(twilioMessage) err := twilioNotifier.Send(twilioMessage)
assert.Nil(t, err) assert.Nil(t, err)

View File

@ -193,7 +193,7 @@ func copyAndCapture(w io.Writer, r io.Reader) ([]byte, error) {
func DurationReadable(d time.Duration) string { func DurationReadable(d time.Duration) string {
if d.Hours() >= 1 { if d.Hours() >= 1 {
return fmt.Sprintf("%0.0f hours and %0.0f minutes", d.Hours(), d.Minutes()) return fmt.Sprintf("%0.0f hours", d.Hours())
} else if d.Minutes() >= 1 { } else if d.Minutes() >= 1 {
return fmt.Sprintf("%0.0f minutes", d.Minutes()) return fmt.Sprintf("%0.0f minutes", d.Minutes())
} else if d.Seconds() >= 1 { } else if d.Seconds() >= 1 {