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:
+74
-36
@@ -416,7 +416,7 @@ func TestSchedule_ReassigningAPartlyTakenWeekAsksOnce(t *testing.T) {
|
||||
mustContain(t, m.confirmPrompt(), "2 of 7 days are assigned to niklas and sam")
|
||||
}
|
||||
|
||||
// ── Ntfy topic ────────────────────────────────────────────────────────────
|
||||
// ── Notification channels ─────────────────────────────────────────────────
|
||||
|
||||
// onUsers puts the model in the Users section with a loaded table.
|
||||
func onUsers(users []api.User) Model {
|
||||
@@ -428,42 +428,96 @@ func onUsers(users []api.User) Model {
|
||||
}
|
||||
|
||||
func userFixtures() []api.User {
|
||||
topic := "terdut-niklas"
|
||||
return []api.User{
|
||||
{ID: 1, Username: "niklas", Email: "niklas@example.com", NtfyTopic: &topic},
|
||||
{ID: 1, Username: "niklas", Email: "niklas@example.com",
|
||||
Channels: []api.NotificationChannel{{ID: 3, Type: "ntfy", Address: "terdut-niklas", Enabled: true}}},
|
||||
{ID: 2, Username: "alex", Email: "alex@example.com"},
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotifyTopic_EditPrefillsTheCurrentTopic(t *testing.T) {
|
||||
func TestChannels_TOpensTheMenuForTheUserUnderTheCursor(t *testing.T) {
|
||||
m, _ := press(t, onUsers(userFixtures()), "t")
|
||||
|
||||
if m.mode != modeUserNotifyEdit {
|
||||
t.Fatalf("expected the topic editor, got mode %v", m.mode)
|
||||
if m.mode != modeChannelMenu {
|
||||
t.Fatalf("expected the channel menu, got mode %v", m.mode)
|
||||
}
|
||||
if m.selectedUser.ID != 1 {
|
||||
t.Errorf("expected the user under the cursor, got %d", m.selectedUser.ID)
|
||||
}
|
||||
// Prefilled, so editing a topic does not mean retyping it from scratch.
|
||||
if got := m.ntfyTopicInput.Value(); got != "terdut-niklas" {
|
||||
t.Errorf("expected the current topic prefilled, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A user with no topic opens an empty field rather than the previous user's.
|
||||
func TestNotifyTopic_EditStartsEmptyWhenUnset(t *testing.T) {
|
||||
m := onUsers(userFixtures())
|
||||
m, _ = press(t, m, "t")
|
||||
func TestChannels_AddStartsEmptyEveryTime(t *testing.T) {
|
||||
m, _ := press(t, onUsers(userFixtures()), "t")
|
||||
m, _ = press(t, m, "a")
|
||||
if m.mode != modeChannelAdd {
|
||||
t.Fatalf("expected the add form, got mode %v", m.mode)
|
||||
}
|
||||
m = typeInto(t, m, "ntfy:abandoned")
|
||||
m, _ = press(t, m, "esc")
|
||||
m.userManageTable.SetCursor(1)
|
||||
if m.mode != modeChannelMenu {
|
||||
t.Fatalf("escape should go back to the menu, got mode %v", m.mode)
|
||||
}
|
||||
|
||||
m, _ = press(t, m, "t")
|
||||
if got := m.ntfyTopicInput.Value(); got != "" {
|
||||
t.Errorf("expected an empty field for a user with no topic, got %q", got)
|
||||
m, _ = press(t, m, "a")
|
||||
if got := m.channelAddInput.Value(); got != "" {
|
||||
t.Errorf("expected an empty field, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotifyTopic_EscapeAbandonsWithoutSaving(t *testing.T) {
|
||||
func TestChannels_AddRejectsASpecWithoutTypeAndAddress(t *testing.T) {
|
||||
for _, spec := range []string{"", "ntfy", "ntfy:", ":topic"} {
|
||||
m, _ := press(t, onUsers(userFixtures()), "t")
|
||||
m, _ = press(t, m, "a")
|
||||
m.channelAddInput.SetValue(spec)
|
||||
|
||||
m, cmd := press(t, m, "enter")
|
||||
if m.mode != modeChannelAdd {
|
||||
t.Errorf("%q: expected to stay on the form, got mode %v", spec, m.mode)
|
||||
}
|
||||
if m.statusMsg == "" {
|
||||
t.Errorf("%q: expected an explanation", spec)
|
||||
}
|
||||
_ = cmd // only the status-clearing tick
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannels_AddSubmitsAndReturnsToTheDashboard(t *testing.T) {
|
||||
m, _ := press(t, onUsers(userFixtures()), "t")
|
||||
m, _ = press(t, m, "a")
|
||||
m.channelAddInput.SetValue("ntfy:my-topic")
|
||||
|
||||
m, cmd := press(t, m, "enter")
|
||||
if cmd == nil {
|
||||
t.Fatal("expected adding a channel to call the server")
|
||||
}
|
||||
if m.mode != modeDashboard {
|
||||
t.Errorf("expected a return to the dashboard, got mode %v", m.mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannels_RemoveNeedsAPositiveID(t *testing.T) {
|
||||
m, _ := press(t, onUsers(userFixtures()), "t")
|
||||
m, _ = press(t, m, "r")
|
||||
if m.mode != modeChannelRemoveByID {
|
||||
t.Fatalf("expected the remove form, got mode %v", m.mode)
|
||||
}
|
||||
|
||||
for _, bad := range []string{"", "abc", "0", "-3"} {
|
||||
m.channelRemoveIn.SetValue(bad)
|
||||
next, _ := press(t, m, "enter")
|
||||
if next.mode != modeChannelRemoveByID {
|
||||
t.Errorf("%q: expected to stay on the form, got mode %v", bad, next.mode)
|
||||
}
|
||||
}
|
||||
|
||||
m.channelRemoveIn.SetValue("3")
|
||||
m, cmd := press(t, m, "enter")
|
||||
if cmd == nil || m.mode != modeDashboard {
|
||||
t.Errorf("expected a valid ID to call the server and return, mode %v", m.mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannels_EscapeAbandonsWithoutSaving(t *testing.T) {
|
||||
m, _ := press(t, onUsers(userFixtures()), "t")
|
||||
m, cmd := press(t, m, "esc")
|
||||
|
||||
@@ -475,23 +529,7 @@ func TestNotifyTopic_EscapeAbandonsWithoutSaving(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Clearing a topic is a real action, not a no-op: it is how a user is taken off
|
||||
// their own topic and back onto the shared fallback. Contrast the snooze prompt,
|
||||
// where an empty value means "I changed my mind".
|
||||
func TestNotifyTopic_EmptyInputStillSubmits(t *testing.T) {
|
||||
m, _ := press(t, onUsers(userFixtures()), "t")
|
||||
m.ntfyTopicInput.SetValue("")
|
||||
|
||||
m, cmd := press(t, m, "enter")
|
||||
if cmd == nil {
|
||||
t.Fatal("expected clearing the topic to call the server")
|
||||
}
|
||||
if m.mode != modeDashboard {
|
||||
t.Errorf("expected a return to the dashboard, got mode %v", m.mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotifyTopic_IsUsersSectionOnly(t *testing.T) {
|
||||
func TestChannels_IsUsersSectionOnly(t *testing.T) {
|
||||
m := sized()
|
||||
m.activeSection = sectionIncidents
|
||||
if next, cmd := press(t, m, "t"); cmd != nil || next.mode != modeDashboard {
|
||||
|
||||
Reference in New Issue
Block a user