Changeset 0.28.6 (#192)

This commit is contained in:
2026-03-15 01:33:38 +00:00
parent 6f0ad1355c
commit bffda043db
59 changed files with 3022 additions and 77 deletions

View File

@@ -12,6 +12,7 @@ import (
"git.gobha.me/xcaliber/chat-switchboard/database"
"git.gobha.me/xcaliber/chat-switchboard/middleware"
"git.gobha.me/xcaliber/chat-switchboard/models"
"git.gobha.me/xcaliber/chat-switchboard/notifications"
"git.gobha.me/xcaliber/chat-switchboard/store"
postgres "git.gobha.me/xcaliber/chat-switchboard/store/postgres"
sqlite "git.gobha.me/xcaliber/chat-switchboard/store/sqlite"
@@ -61,6 +62,16 @@ func setupNotifHarness(t *testing.T) *notifHarness {
protected.PUT("/notifications/preferences/:type", notifH.SetPreference)
protected.DELETE("/notifications/preferences/:type", notifH.DeletePreference)
// Admin broadcast route (v0.28.6)
admin := api.Group("/admin")
admin.Use(middleware.Auth(cfg, stores.Users, userCache))
admin.Use(middleware.RequireAdmin())
admin.POST("/notifications/broadcast", notifH.Broadcast)
// Set up notification service singleton for Broadcast handler
notifSvc := notifications.NewService(stores.Notifications, nil)
notifications.SetDefault(notifSvc)
// Seed admin
adminID := database.SeedTestUser(t, "notif-admin", "notif-admin@test.com")
database.TestDB.Exec(dialectSQL("UPDATE users SET role = 'admin', is_active = true WHERE id = $1"), adminID)
@@ -548,6 +559,110 @@ func TestNotifications_Unauthenticated(t *testing.T) {
}
}
// ── Admin Broadcast (v0.28.6) ────────────
func TestBroadcast_Success(t *testing.T) {
h := setupNotifHarness(t)
resp := h.request("POST", "/api/v1/admin/notifications/broadcast", h.adminToken, map[string]interface{}{
"title": "Maintenance Tonight",
"message": "Servers will be down from 2-4 AM.",
"level": "warning",
})
if resp.Code != http.StatusOK {
t.Fatalf("broadcast: got %d, body: %s", resp.Code, resp.Body.String())
}
var body map[string]interface{}
json.NewDecoder(resp.Body).Decode(&body)
if body["message"] != "broadcast sent" {
t.Errorf("expected 'broadcast sent', got %v", body["message"])
}
count := body["count"].(float64)
if count < 2 {
t.Errorf("expected at least 2 recipients (admin+user), got %v", count)
}
// Verify notification created for regular user
resp = h.request("GET", "/api/v1/notifications", h.userToken, nil)
if resp.Code != http.StatusOK {
t.Fatalf("list notifications: got %d", resp.Code)
}
var listBody map[string]interface{}
json.NewDecoder(resp.Body).Decode(&listBody)
items := listBody["data"].([]interface{})
if len(items) == 0 {
t.Fatal("user should have received the broadcast notification")
}
n := items[0].(map[string]interface{})
if n["type"] != "system.announcement" {
t.Errorf("notification type: got %v, want system.announcement", n["type"])
}
if n["title"] != "Maintenance Tonight" {
t.Errorf("notification title: got %v", n["title"])
}
}
func TestBroadcast_NonAdmin403(t *testing.T) {
h := setupNotifHarness(t)
resp := h.request("POST", "/api/v1/admin/notifications/broadcast", h.userToken, map[string]interface{}{
"title": "Unauthorized",
"message": "Should fail.",
})
if resp.Code != http.StatusForbidden {
t.Fatalf("non-admin broadcast: want 403, got %d", resp.Code)
}
}
func TestBroadcast_MissingTitle(t *testing.T) {
h := setupNotifHarness(t)
resp := h.request("POST", "/api/v1/admin/notifications/broadcast", h.adminToken, map[string]interface{}{
"message": "No title.",
})
if resp.Code != http.StatusBadRequest {
t.Fatalf("missing title: want 400, got %d", resp.Code)
}
}
func TestBroadcast_MissingMessage(t *testing.T) {
h := setupNotifHarness(t)
resp := h.request("POST", "/api/v1/admin/notifications/broadcast", h.adminToken, map[string]interface{}{
"title": "No message.",
})
if resp.Code != http.StatusBadRequest {
t.Fatalf("missing message: want 400, got %d", resp.Code)
}
}
func TestBroadcast_InvalidLevel(t *testing.T) {
h := setupNotifHarness(t)
resp := h.request("POST", "/api/v1/admin/notifications/broadcast", h.adminToken, map[string]interface{}{
"title": "Bad level",
"message": "Test",
"level": "extreme",
})
if resp.Code != http.StatusBadRequest {
t.Fatalf("invalid level: want 400, got %d", resp.Code)
}
}
func TestBroadcast_DefaultLevel(t *testing.T) {
h := setupNotifHarness(t)
// Omit level — should default to info and succeed
resp := h.request("POST", "/api/v1/admin/notifications/broadcast", h.adminToken, map[string]interface{}{
"title": "Info broadcast",
"message": "Default level test.",
})
if resp.Code != http.StatusOK {
t.Fatalf("default level: want 200, got %d, body: %s", resp.Code, resp.Body.String())
}
}
// ── helpers ────────────────────────────────
func keys(m map[string]json.RawMessage) []string {