Manage notification channels instead of an ntfy topic
Mirrors the server's channels API: add/remove channels per user from the Users section. Requires the matching server release. Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
This commit is contained in:
@@ -184,14 +184,17 @@ func TestEventLabel_KnownTypes(t *testing.T) {
|
||||
"Notified bo (triggered)"},
|
||||
{api.IncidentEvent{Type: api.EventNotified, Username: "bo", Detail: "reminder"},
|
||||
"Notified bo (reminder)"},
|
||||
// On a notification, no user means the shared fallback topic — not that
|
||||
// The server names the channel type after the kind.
|
||||
{api.IncidentEvent{Type: api.EventNotified, Username: "bo", Detail: "triggered via ntfy"},
|
||||
"Notified bo (triggered via ntfy)"},
|
||||
// On a notification, no user means the shared fallback — not that
|
||||
// the server acted on its own.
|
||||
{api.IncidentEvent{Type: api.EventNotified, Detail: "triggered"},
|
||||
"Notified the fallback topic (triggered)"},
|
||||
{api.IncidentEvent{Type: api.EventNotifyFailed, Username: "bo", Detail: "triggered: ntfy returned 502"},
|
||||
"Notified the fallback (triggered)"},
|
||||
{api.IncidentEvent{Type: api.EventNotifyFailed, Username: "bo", Detail: "triggered via ntfy: ntfy returned 502"},
|
||||
"Notification to bo failed"},
|
||||
{api.IncidentEvent{Type: api.EventNotifyFailed, Detail: "triggered: no route to host"},
|
||||
"Notification to the fallback topic failed"},
|
||||
"Notification to the fallback failed"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.event.Type, func(t *testing.T) {
|
||||
@@ -216,14 +219,45 @@ func TestIncidentDetail_RendersNotifications(t *testing.T) {
|
||||
mustContain(t, got, "Notified niklas (triggered)", "Notification to niklas failed")
|
||||
}
|
||||
|
||||
func TestUserNotifyEdit_SaysWhatAnEmptyValueDoes(t *testing.T) {
|
||||
func TestChannelMenu_ListsKindsAndSaysWhenThereAreNone(t *testing.T) {
|
||||
m := sized()
|
||||
m.mode = modeUserNotifyEdit
|
||||
m.mode = modeChannelMenu
|
||||
m.selectedUser = api.User{ID: 1, Username: "niklas"}
|
||||
|
||||
mustContain(t, m.View(), "niklas", "empty to clear it", "fallback topic")
|
||||
// The footer has to repeat it: that is where the reader looks for what a key does.
|
||||
mustContain(t, m.renderFooter(), "empty clears the topic")
|
||||
mustContain(t, m.View(), "niklas", "None", "fallback", "add a channel", "remove a channel")
|
||||
mustContain(t, m.renderFooter(), "add a channel", "remove by ID")
|
||||
|
||||
// An administrator sees the kind and ID of somebody else's channel, never where it goes.
|
||||
m.selectedUser.Channels = []api.NotificationChannel{{ID: 7, Type: "ntfy", Enabled: false}}
|
||||
mustContain(t, m.View(), "#7", "ntfy", "(address hidden)", "(off)")
|
||||
}
|
||||
|
||||
func TestChannelAdd_ExplainsTheSpec(t *testing.T) {
|
||||
m := sized()
|
||||
m.mode = modeChannelAdd
|
||||
m.selectedUser = api.User{ID: 1, Username: "niklas"}
|
||||
|
||||
mustContain(t, m.View(), "niklas", "type", "colon")
|
||||
mustContain(t, m.renderFooter(), "type:address")
|
||||
}
|
||||
|
||||
func TestParseChannelSpec(t *testing.T) {
|
||||
for spec, want := range map[string][2]string{
|
||||
"ntfy:my-topic": {"ntfy", "my-topic"},
|
||||
" ntfy : my-topic ": {"ntfy", "my-topic"},
|
||||
"mail:a@b.example": {"mail", "a@b.example"},
|
||||
"ntfy:a:b": {"ntfy", "a:b"},
|
||||
} {
|
||||
typ, address, ok := parseChannelSpec(spec)
|
||||
if !ok || typ != want[0] || address != want[1] {
|
||||
t.Errorf("%q: got %q, %q, %v", spec, typ, address, ok)
|
||||
}
|
||||
}
|
||||
for _, bad := range []string{"", "ntfy", "ntfy:", ":topic", " "} {
|
||||
if _, _, ok := parseChannelSpec(bad); ok {
|
||||
t.Errorf("%q should not parse", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertDetail_SaysItIsReadOnlyAndLinksTheIncident(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user