Skip to content

Commit e5067bc

Browse files
committed
Add notification destination sender tests
1 parent f062159 commit e5067bc

2 files changed

Lines changed: 340 additions & 0 deletions

File tree

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
package googlechat
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"io"
8+
"net/http"
9+
"net/http/httptest"
10+
"net/url"
11+
"testing"
12+
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
"github.com/target/goalert/config"
16+
"github.com/target/goalert/notification"
17+
"github.com/target/goalert/notification/nfymsg"
18+
)
19+
20+
func testConfig() config.Config {
21+
var cfg config.Config
22+
cfg.General.PublicURL = "https://goalert.example"
23+
return cfg
24+
}
25+
26+
type rewriteTransport struct {
27+
target *url.URL
28+
rt http.RoundTripper
29+
}
30+
31+
func (t rewriteTransport) RoundTrip(req *http.Request) (*http.Response, error) {
32+
clone := req.Clone(req.Context())
33+
clone.URL.Scheme = t.target.Scheme
34+
clone.URL.Host = t.target.Host
35+
clone.URL.Path = req.URL.Path
36+
clone.URL.RawPath = req.URL.RawPath
37+
clone.URL.RawQuery = req.URL.RawQuery
38+
clone.Host = t.target.Host
39+
return t.rt.RoundTrip(clone)
40+
}
41+
42+
func TestFormatScheduleOnCallUsers(t *testing.T) {
43+
ctx := testConfig().Context(context.Background())
44+
msg := notification.ScheduleOnCallUsers{
45+
ScheduleName: "Primary Schedule",
46+
ScheduleURL: "https://goalert.example/schedules/1",
47+
Users: []notification.User{
48+
{ID: "b", Name: "Bravo"},
49+
{ID: "a", Name: "Alpha"},
50+
},
51+
}
52+
53+
assert.Equal(t,
54+
"GoAlert on-call shift changed\nSchedule: Primary Schedule\nNow on-call: Alpha, Bravo\nLink: https://goalert.example/schedules/1",
55+
formatGoAlertMessage(ctx, msg),
56+
)
57+
}
58+
59+
func TestValidateFieldWebhookURL(t *testing.T) {
60+
cfg := testConfig()
61+
ctx := cfg.Context(context.Background())
62+
sender := &Sender{}
63+
64+
valid := "https://chat.googleapis.com/v1/spaces/AAA/messages?key=k&token=t"
65+
require.NoError(t, sender.ValidateField(ctx, FieldWebhookURL, valid))
66+
67+
t.Run("invalid host", func(t *testing.T) {
68+
err := sender.ValidateField(ctx, FieldWebhookURL, "https://example.com/v1/spaces/AAA/messages?key=k&token=t")
69+
require.Error(t, err)
70+
assert.Contains(t, err.Error(), "Google Chat incoming webhook URL")
71+
})
72+
73+
t.Run("missing token", func(t *testing.T) {
74+
err := sender.ValidateField(ctx, FieldWebhookURL, "https://chat.googleapis.com/v1/spaces/AAA/messages?key=k")
75+
require.Error(t, err)
76+
assert.Contains(t, err.Error(), "key and token")
77+
})
78+
}
79+
80+
func TestSendMessage(t *testing.T) {
81+
type result struct {
82+
state notification.State
83+
detail string
84+
}
85+
86+
tests := []struct {
87+
name string
88+
message notification.Message
89+
wantText string
90+
statusCode int
91+
want result
92+
}{
93+
{
94+
name: "alert",
95+
message: notification.Alert{
96+
Base: nfymsg.Base{
97+
ID: "msg-1",
98+
Dest: NewGoogleChatDest("https://chat.googleapis.com/v1/spaces/AAA/messages?key=k&token=t"),
99+
},
100+
AlertID: 42,
101+
Summary: "Database is down",
102+
Details: "postgres is unreachable",
103+
ServiceName: "Payments",
104+
},
105+
wantText: "GoAlert alert\nAlert: #42 Database is down\nService: Payments\nDetails: postgres is unreachable\nLink: https://goalert.example/alerts/42",
106+
want: result{state: notification.StateSent},
107+
},
108+
{
109+
name: "alert bundle",
110+
message: notification.AlertBundle{
111+
Base: nfymsg.Base{
112+
ID: "msg-2",
113+
Dest: NewGoogleChatDest("https://chat.googleapis.com/v1/spaces/AAA/messages?key=k&token=t"),
114+
},
115+
ServiceID: "svc-1",
116+
ServiceName: "Payments",
117+
Count: 3,
118+
},
119+
wantText: "GoAlert alert bundle\nService: Payments\nCount: 3 unacknowledged alerts\nLink: https://goalert.example/services/svc-1/alerts",
120+
want: result{state: notification.StateSent},
121+
},
122+
{
123+
name: "alert status",
124+
message: notification.AlertStatus{
125+
Base: nfymsg.Base{
126+
ID: "msg-3",
127+
Dest: NewGoogleChatDest("https://chat.googleapis.com/v1/spaces/AAA/messages?key=k&token=t"),
128+
},
129+
AlertID: 42,
130+
Summary: "Database is down",
131+
LogEntry: "acknowledged by on-call",
132+
NewAlertState: notification.AlertStateAcknowledged,
133+
},
134+
wantText: "GoAlert alert update\nAlert: #42 Database is down\nState: acknowledged\nLog: acknowledged by on-call\nLink: https://goalert.example/alerts/42",
135+
want: result{state: notification.StateSent},
136+
},
137+
{
138+
name: "on-call",
139+
message: notification.ScheduleOnCallUsers{
140+
Base: nfymsg.Base{
141+
ID: "msg-4",
142+
Dest: NewGoogleChatDest("https://chat.googleapis.com/v1/spaces/AAA/messages?key=k&token=t"),
143+
},
144+
ScheduleName: "Primary Schedule",
145+
ScheduleURL: "https://goalert.example/schedules/1",
146+
Users: []notification.User{
147+
{ID: "b", Name: "Bravo"},
148+
{ID: "a", Name: "Alpha"},
149+
},
150+
},
151+
wantText: "GoAlert on-call shift changed\nSchedule: Primary Schedule\nNow on-call: Alpha, Bravo\nLink: https://goalert.example/schedules/1",
152+
want: result{state: notification.StateSent},
153+
},
154+
{
155+
name: "temporary failure",
156+
message: notification.ScheduleOnCallUsers{
157+
Base: nfymsg.Base{
158+
ID: "msg-5",
159+
Dest: NewGoogleChatDest("https://chat.googleapis.com/v1/spaces/AAA/messages?key=k&token=t"),
160+
},
161+
ScheduleName: "Primary Schedule",
162+
ScheduleURL: "https://goalert.example/schedules/1",
163+
},
164+
wantText: "GoAlert on-call shift changed\nSchedule: Primary Schedule\nNow on-call: Nobody\nLink: https://goalert.example/schedules/1",
165+
statusCode: http.StatusInternalServerError,
166+
want: result{state: notification.StateFailedTemp, detail: "500 Internal Server Error"},
167+
},
168+
{
169+
name: "permanent failure",
170+
message: notification.ScheduleOnCallUsers{
171+
Base: nfymsg.Base{
172+
ID: "msg-6",
173+
Dest: NewGoogleChatDest("https://chat.googleapis.com/v1/spaces/AAA/messages?key=k&token=t"),
174+
},
175+
ScheduleName: "Primary Schedule",
176+
ScheduleURL: "https://goalert.example/schedules/1",
177+
},
178+
wantText: "GoAlert on-call shift changed\nSchedule: Primary Schedule\nNow on-call: Nobody\nLink: https://goalert.example/schedules/1",
179+
statusCode: http.StatusForbidden,
180+
want: result{state: notification.StateFailedPerm, detail: "403 Forbidden"},
181+
},
182+
}
183+
184+
for _, tt := range tests {
185+
t.Run(tt.name, func(t *testing.T) {
186+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
187+
assert.Equal(t, http.MethodPost, r.Method)
188+
assert.Equal(t, "application/json; charset=UTF-8", r.Header.Get("Content-Type"))
189+
190+
data, err := io.ReadAll(r.Body)
191+
require.NoError(t, err)
192+
193+
var payload ChatMessage
194+
require.NoError(t, json.NewDecoder(bytes.NewReader(data)).Decode(&payload))
195+
assert.Equal(t, tt.wantText, payload.Text)
196+
197+
if tt.statusCode != 0 && tt.statusCode != http.StatusOK {
198+
w.WriteHeader(tt.statusCode)
199+
_, _ = io.WriteString(w, "chat error")
200+
return
201+
}
202+
203+
w.WriteHeader(http.StatusOK)
204+
}))
205+
defer srv.Close()
206+
207+
targetURL, err := url.Parse(srv.URL)
208+
require.NoError(t, err)
209+
210+
client := &http.Client{
211+
Transport: rewriteTransport{
212+
target: targetURL,
213+
rt: http.DefaultTransport,
214+
},
215+
}
216+
217+
cfg := testConfig()
218+
ctx := cfg.Context(context.Background())
219+
sender := NewSender(ctx, client)
220+
221+
sent, err := sender.SendMessage(ctx, tt.message)
222+
require.NoError(t, err)
223+
require.NotNil(t, sent)
224+
assert.Equal(t, tt.want.state, sent.State)
225+
if tt.want.detail == "" {
226+
assert.Empty(t, sent.StateDetails)
227+
} else {
228+
assert.Contains(t, sent.StateDetails, tt.want.detail)
229+
}
230+
})
231+
}
232+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package webhook
2+
3+
import (
4+
"context"
5+
"io"
6+
"net/http"
7+
"net/http/httptest"
8+
"net/url"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
"github.com/target/goalert/config"
14+
"github.com/target/goalert/notification"
15+
"github.com/target/goalert/notification/nfymsg"
16+
)
17+
18+
func TestCustomSender_SendMessage(t *testing.T) {
19+
type result struct {
20+
state notification.State
21+
detail string
22+
}
23+
24+
tests := []struct {
25+
name string
26+
statusCode int
27+
want result
28+
}{
29+
{
30+
name: "success",
31+
statusCode: http.StatusOK,
32+
want: result{state: notification.StateSent},
33+
},
34+
{
35+
name: "temporary failure",
36+
statusCode: http.StatusInternalServerError,
37+
want: result{state: notification.StateFailedTemp, detail: "500 Internal Server Error"},
38+
},
39+
{
40+
name: "permanent failure",
41+
statusCode: http.StatusForbidden,
42+
want: result{state: notification.StateFailedPerm, detail: "403 Forbidden"},
43+
},
44+
}
45+
46+
for _, tt := range tests {
47+
t.Run(tt.name, func(t *testing.T) {
48+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
49+
assert.Equal(t, http.MethodPost, r.Method)
50+
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
51+
52+
body, err := io.ReadAll(r.Body)
53+
require.NoError(t, err)
54+
assert.Contains(t, string(body), "Database is down")
55+
assert.Contains(t, string(body), "Payments")
56+
57+
if tt.statusCode != http.StatusOK {
58+
w.WriteHeader(tt.statusCode)
59+
_, _ = io.WriteString(w, "custom webhook error")
60+
return
61+
}
62+
w.WriteHeader(http.StatusOK)
63+
}))
64+
defer srv.Close()
65+
66+
cfg := config.Config{}
67+
cfg.General.PublicURL = "https://goalert.example"
68+
ctx := cfg.Context(context.Background())
69+
70+
targetURL, err := url.Parse(srv.URL)
71+
require.NoError(t, err)
72+
73+
sender := &CustomSender{Client: srv.Client()}
74+
msg := notification.Alert{
75+
Base: nfymsg.Base{
76+
ID: "msg-1",
77+
Dest: NewCustomWebhookDest(targetURL.String(), `{"text":"{{.Summary}} - {{.ServiceName}}"}`, "application/json"),
78+
},
79+
AlertID: 42,
80+
Summary: "Database is down",
81+
Details: "postgres is unreachable",
82+
ServiceName: "Payments",
83+
}
84+
85+
sent, err := sender.SendMessage(ctx, msg)
86+
require.NoError(t, err)
87+
require.NotNil(t, sent)
88+
assert.Equal(t, tt.want.state, sent.State)
89+
if tt.want.detail == "" {
90+
assert.Empty(t, sent.StateDetails)
91+
} else {
92+
assert.Contains(t, sent.StateDetails, tt.want.detail)
93+
}
94+
})
95+
}
96+
}
97+
98+
func TestCustomSender_ValidateField(t *testing.T) {
99+
cfg := config.Config{}
100+
cfg.General.PublicURL = "https://goalert.example"
101+
ctx := cfg.Context(context.Background())
102+
sender := &CustomSender{}
103+
104+
require.NoError(t, sender.ValidateField(ctx, FieldWebhookURL, "https://example.com"))
105+
require.NoError(t, sender.ValidateField(ctx, FieldBodyTemplate, `{"text":"{{.Summary}}"}`))
106+
require.NoError(t, sender.ValidateField(ctx, FieldContentType, "application/json"))
107+
require.NoError(t, sender.ValidateField(ctx, FieldContentType, ""))
108+
}

0 commit comments

Comments
 (0)