|
|
|
@@ -1,425 +0,0 @@
|
|
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"database/sql"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
|
|
|
"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
|
|
|
|
|
"github.com/Molecule-AI/molecule-monorepo/platform/internal/events"
|
|
|
|
|
"github.com/Molecule-AI/molecule-monorepo/platform/internal/ws"
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// setupBroadcastDB uses QueryMatcherEqual so SQL strings with quoted literals
|
|
|
|
|
// (e.g. status != 'removed') are compared verbatim, not as regex.
|
|
|
|
|
func setupBroadcastDB(t *testing.T) sqlmock.Sqlmock {
|
|
|
|
|
t.Helper()
|
|
|
|
|
mockDB, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
|
|
|
}
|
|
|
|
|
prevDB := db.DB
|
|
|
|
|
db.DB = mockDB
|
|
|
|
|
t.Cleanup(func() { db.DB = prevDB; mockDB.Close() })
|
|
|
|
|
return mock
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// validUUID is a properly formatted test UUID.
|
|
|
|
|
const broadcastTestUUID = "bbbbbbbb-0001-0001-0001-000000000001"
|
|
|
|
|
|
|
|
|
|
// buildBroadcastCtx creates a gin.Context wired for POST /workspaces/:id/broadcast.
|
|
|
|
|
func buildBroadcastCtx(id, body string) (*gin.Context, *httptest.ResponseRecorder) {
|
|
|
|
|
w := httptest.NewRecorder()
|
|
|
|
|
c, _ := gin.CreateTestContext(w)
|
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/workspaces/"+id+"/broadcast", bytes.NewBufferString(body))
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
|
c.Request = req.WithContext(context.Background())
|
|
|
|
|
c.Params = gin.Params{{Key: "id", Value: id}}
|
|
|
|
|
return c, w
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Pure function ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestBroadcastTruncate(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
s string
|
|
|
|
|
max int
|
|
|
|
|
want string
|
|
|
|
|
}{
|
|
|
|
|
{"empty string", "", 10, ""},
|
|
|
|
|
{"under limit", "hello", 10, "hello"},
|
|
|
|
|
{"exactly at limit", "hello", 5, "hello"},
|
|
|
|
|
{"over limit", "hello world", 5, "hello…"},
|
|
|
|
|
{"unicode over limit", "こんにちは世界", 5, "こんにちは…"},
|
|
|
|
|
{"ascii over limit", "abcdefghij", 5, "abcde…"},
|
|
|
|
|
}
|
|
|
|
|
for _, tc := range tests {
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
|
got := broadcastTruncate(tc.s, tc.max)
|
|
|
|
|
if got != tc.want {
|
|
|
|
|
t.Errorf("broadcastTruncate(%q, %d) = %q; want %q", tc.s, tc.max, got, tc.want)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Validation ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestBroadcast_InvalidWorkspaceID(t *testing.T) {
|
|
|
|
|
c, w := buildBroadcastCtx("not-a-uuid", `{"message":"hello"}`)
|
|
|
|
|
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
|
|
|
|
|
handler.Broadcast(c)
|
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
|
|
|
t.Errorf("want 400, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBroadcast_MissingMessage(t *testing.T) {
|
|
|
|
|
mock := setupBroadcastDB(t)
|
|
|
|
|
c, w := buildBroadcastCtx(broadcastTestUUID, `{}`)
|
|
|
|
|
|
|
|
|
|
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
|
|
|
|
|
handler.Broadcast(c)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
|
|
|
t.Errorf("want 400, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
|
|
|
t.Errorf("unmet mock expectations: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBroadcast_MalformedJSON(t *testing.T) {
|
|
|
|
|
mock := setupBroadcastDB(t)
|
|
|
|
|
c, w := buildBroadcastCtx(broadcastTestUUID, `not json`)
|
|
|
|
|
|
|
|
|
|
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
|
|
|
|
|
handler.Broadcast(c)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
|
|
|
t.Errorf("want 400, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
|
|
|
t.Errorf("unmet mock expectations: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Auth / Authz ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestBroadcast_WorkspaceNotFound(t *testing.T) {
|
|
|
|
|
mock := setupBroadcastDB(t)
|
|
|
|
|
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello"}`)
|
|
|
|
|
|
|
|
|
|
// Workspace lookup returns no rows.
|
|
|
|
|
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
|
|
|
|
|
WithArgs(broadcastTestUUID).
|
|
|
|
|
WillReturnError(sql.ErrNoRows)
|
|
|
|
|
|
|
|
|
|
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
|
|
|
|
|
handler.Broadcast(c)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
|
|
|
t.Errorf("want 404, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
|
|
|
t.Errorf("unmet mock expectations: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBroadcast_WorkspaceLookupQueryError(t *testing.T) {
|
|
|
|
|
mock := setupBroadcastDB(t)
|
|
|
|
|
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello"}`)
|
|
|
|
|
|
|
|
|
|
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
|
|
|
|
|
WithArgs(broadcastTestUUID).
|
|
|
|
|
WillReturnError(sql.ErrConnDone)
|
|
|
|
|
|
|
|
|
|
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
|
|
|
|
|
handler.Broadcast(c)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
|
|
|
t.Errorf("want 404, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
|
|
|
t.Errorf("unmet mock expectations: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBroadcast_BroadcastDisabled(t *testing.T) {
|
|
|
|
|
mock := setupBroadcastDB(t)
|
|
|
|
|
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello"}`)
|
|
|
|
|
|
|
|
|
|
// Workspace found but broadcast_enabled=false.
|
|
|
|
|
rows := sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
|
|
|
|
|
AddRow("test-workspace", false)
|
|
|
|
|
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
|
|
|
|
|
WithArgs(broadcastTestUUID).
|
|
|
|
|
WillReturnRows(rows)
|
|
|
|
|
|
|
|
|
|
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
|
|
|
|
|
handler.Broadcast(c)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusForbidden {
|
|
|
|
|
t.Errorf("want 403, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
|
|
|
t.Errorf("unmet mock expectations: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── DB error paths ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestBroadcast_RecipientQueryError(t *testing.T) {
|
|
|
|
|
mock := setupBroadcastDB(t)
|
|
|
|
|
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello"}`)
|
|
|
|
|
|
|
|
|
|
// Workspace lookup succeeds with broadcast_enabled=true.
|
|
|
|
|
rows := sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
|
|
|
|
|
AddRow("test-workspace", true)
|
|
|
|
|
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
|
|
|
|
|
WithArgs(broadcastTestUUID).
|
|
|
|
|
WillReturnRows(rows)
|
|
|
|
|
|
|
|
|
|
// Recipient query fails.
|
|
|
|
|
mock.ExpectQuery("SELECT id FROM workspaces WHERE status != 'removed' AND id != $1").
|
|
|
|
|
WithArgs(broadcastTestUUID).
|
|
|
|
|
WillReturnError(sql.ErrConnDone)
|
|
|
|
|
|
|
|
|
|
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
|
|
|
|
|
handler.Broadcast(c)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
|
|
|
t.Errorf("want 500, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
|
|
|
t.Errorf("unmet mock expectations: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBroadcast_RecipientRowsError(t *testing.T) {
|
|
|
|
|
mock := setupBroadcastDB(t)
|
|
|
|
|
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello"}`)
|
|
|
|
|
|
|
|
|
|
rows := sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
|
|
|
|
|
AddRow("test-workspace", true)
|
|
|
|
|
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
|
|
|
|
|
WithArgs(broadcastTestUUID).
|
|
|
|
|
WillReturnRows(rows)
|
|
|
|
|
|
|
|
|
|
// Recipient query succeeds but rows.Err() fails.
|
|
|
|
|
badRows := sqlmock.NewRows([]string{"id"}).AddRow("ws-2").RowError(0, sql.ErrConnDone)
|
|
|
|
|
mock.ExpectQuery("SELECT id FROM workspaces WHERE status != 'removed' AND id != $1").
|
|
|
|
|
WithArgs(broadcastTestUUID).
|
|
|
|
|
WillReturnRows(badRows)
|
|
|
|
|
|
|
|
|
|
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
|
|
|
|
|
handler.Broadcast(c)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
|
|
|
t.Errorf("want 500, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
|
|
|
t.Errorf("unmet mock expectations: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Success paths ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestBroadcast_Success_OneRecipient(t *testing.T) {
|
|
|
|
|
mock := setupBroadcastDB(t)
|
|
|
|
|
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello world"}`)
|
|
|
|
|
|
|
|
|
|
// Workspace lookup.
|
|
|
|
|
wsRows := sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
|
|
|
|
|
AddRow("sender-workspace", true)
|
|
|
|
|
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
|
|
|
|
|
WithArgs(broadcastTestUUID).
|
|
|
|
|
WillReturnRows(wsRows)
|
|
|
|
|
|
|
|
|
|
// Recipient query: one recipient.
|
|
|
|
|
recipRows := sqlmock.NewRows([]string{"id"}).AddRow("ws-recipient-1")
|
|
|
|
|
mock.ExpectQuery("SELECT id FROM workspaces WHERE status != 'removed' AND id != $1").
|
|
|
|
|
WithArgs(broadcastTestUUID).
|
|
|
|
|
WillReturnRows(recipRows)
|
|
|
|
|
|
|
|
|
|
// Activity log insert for recipient.
|
|
|
|
|
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, source_id, summary, status) VALUES ($1, 'broadcast_receive', 'broadcast', $2, $3, 'ok')").
|
|
|
|
|
WithArgs("ws-recipient-1", broadcastTestUUID, sqlmock.AnyArg()).
|
|
|
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
|
|
|
|
|
|
// Activity log insert for sender (broadcast_sent).
|
|
|
|
|
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, summary, status) VALUES ($1, 'broadcast_sent', 'broadcast', $2, 'ok')").
|
|
|
|
|
WithArgs(broadcastTestUUID, sqlmock.AnyArg()).
|
|
|
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
|
|
|
|
|
|
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
|
|
|
|
|
handler.Broadcast(c)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Errorf("want 200, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
|
|
|
t.Errorf("unmet mock expectations: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBroadcast_Success_NoRecipients(t *testing.T) {
|
|
|
|
|
mock := setupBroadcastDB(t)
|
|
|
|
|
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello"}`)
|
|
|
|
|
|
|
|
|
|
wsRows := sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
|
|
|
|
|
AddRow("solo-workspace", true)
|
|
|
|
|
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
|
|
|
|
|
WithArgs(broadcastTestUUID).
|
|
|
|
|
WillReturnRows(wsRows)
|
|
|
|
|
|
|
|
|
|
// No recipients.
|
|
|
|
|
recipRows := sqlmock.NewRows([]string{"id"})
|
|
|
|
|
mock.ExpectQuery("SELECT id FROM workspaces WHERE status != 'removed' AND id != $1").
|
|
|
|
|
WithArgs(broadcastTestUUID).
|
|
|
|
|
WillReturnRows(recipRows)
|
|
|
|
|
|
|
|
|
|
// Activity log insert for sender (broadcast_sent).
|
|
|
|
|
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, summary, status) VALUES ($1, 'broadcast_sent', 'broadcast', $2, 'ok')").
|
|
|
|
|
WithArgs(broadcastTestUUID, sqlmock.AnyArg()).
|
|
|
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
|
|
|
|
|
|
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
|
|
|
|
|
handler.Broadcast(c)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Errorf("want 200, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
|
|
|
t.Errorf("unmet mock expectations: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBroadcast_Success_MultipleRecipients(t *testing.T) {
|
|
|
|
|
mock := setupBroadcastDB(t)
|
|
|
|
|
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello"}`)
|
|
|
|
|
|
|
|
|
|
wsRows := sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
|
|
|
|
|
AddRow("broadcaster", true)
|
|
|
|
|
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
|
|
|
|
|
WithArgs(broadcastTestUUID).
|
|
|
|
|
WillReturnRows(wsRows)
|
|
|
|
|
|
|
|
|
|
// Three recipients.
|
|
|
|
|
recipRows := sqlmock.NewRows([]string{"id"}).
|
|
|
|
|
AddRow("ws-1").AddRow("ws-2").AddRow("ws-3")
|
|
|
|
|
mock.ExpectQuery("SELECT id FROM workspaces WHERE status != 'removed' AND id != $1").
|
|
|
|
|
WithArgs(broadcastTestUUID).
|
|
|
|
|
WillReturnRows(recipRows)
|
|
|
|
|
|
|
|
|
|
// Each recipient gets a broadcast_receive log.
|
|
|
|
|
for _, rid := range []string{"ws-1", "ws-2", "ws-3"} {
|
|
|
|
|
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, source_id, summary, status) VALUES ($1, 'broadcast_receive', 'broadcast', $2, $3, 'ok')").
|
|
|
|
|
WithArgs(rid, broadcastTestUUID, sqlmock.AnyArg()).
|
|
|
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sender log.
|
|
|
|
|
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, summary, status) VALUES ($1, 'broadcast_sent', 'broadcast', $2, 'ok')").
|
|
|
|
|
WithArgs(broadcastTestUUID, sqlmock.AnyArg()).
|
|
|
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
|
|
|
|
|
|
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
|
|
|
|
|
handler.Broadcast(c)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Errorf("want 200, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
|
|
|
t.Errorf("unmet mock expectations: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Recipient insert failure (logged, continues) ─────────────────────────────
|
|
|
|
|
|
|
|
|
|
func TestBroadcast_RecipientInsertError_ContinuesAndSucceeds(t *testing.T) {
|
|
|
|
|
mock := setupBroadcastDB(t)
|
|
|
|
|
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello"}`)
|
|
|
|
|
|
|
|
|
|
wsRows := sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
|
|
|
|
|
AddRow("broadcaster", true)
|
|
|
|
|
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
|
|
|
|
|
WithArgs(broadcastTestUUID).
|
|
|
|
|
WillReturnRows(wsRows)
|
|
|
|
|
|
|
|
|
|
// Two recipients.
|
|
|
|
|
recipRows := sqlmock.NewRows([]string{"id"}).
|
|
|
|
|
AddRow("ws-1").AddRow("ws-2")
|
|
|
|
|
mock.ExpectQuery("SELECT id FROM workspaces WHERE status != 'removed' AND id != $1").
|
|
|
|
|
WithArgs(broadcastTestUUID).
|
|
|
|
|
WillReturnRows(recipRows)
|
|
|
|
|
|
|
|
|
|
// First recipient insert fails (logged, continues).
|
|
|
|
|
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, source_id, summary, status) VALUES ($1, 'broadcast_receive', 'broadcast', $2, $3, 'ok')").
|
|
|
|
|
WithArgs("ws-1", broadcastTestUUID, sqlmock.AnyArg()).
|
|
|
|
|
WillReturnError(sql.ErrConnDone)
|
|
|
|
|
|
|
|
|
|
// Second recipient insert succeeds.
|
|
|
|
|
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, source_id, summary, status) VALUES ($1, 'broadcast_receive', 'broadcast', $2, $3, 'ok')").
|
|
|
|
|
WithArgs("ws-2", broadcastTestUUID, sqlmock.AnyArg()).
|
|
|
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
|
|
|
|
|
|
// Sender log.
|
|
|
|
|
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, summary, status) VALUES ($1, 'broadcast_sent', 'broadcast', $2, 'ok')").
|
|
|
|
|
WithArgs(broadcastTestUUID, sqlmock.AnyArg()).
|
|
|
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
|
|
|
|
|
|
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
|
|
|
|
|
handler.Broadcast(c)
|
|
|
|
|
|
|
|
|
|
// Handler returns 200 even though one insert failed — it logs and continues.
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Errorf("want 200 despite insert error, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
|
|
|
t.Errorf("unmet mock expectations: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Sender activity log insert failure (logged, still 200) ───────────────────
|
|
|
|
|
|
|
|
|
|
func TestBroadcast_SenderLogInsertError_Still200(t *testing.T) {
|
|
|
|
|
mock := setupBroadcastDB(t)
|
|
|
|
|
c, w := buildBroadcastCtx(broadcastTestUUID, `{"message":"hello"}`)
|
|
|
|
|
|
|
|
|
|
wsRows := sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
|
|
|
|
|
AddRow("broadcaster", true)
|
|
|
|
|
mock.ExpectQuery("SELECT name, broadcast_enabled FROM workspaces WHERE id = $1 AND status != 'removed'").
|
|
|
|
|
WithArgs(broadcastTestUUID).
|
|
|
|
|
WillReturnRows(wsRows)
|
|
|
|
|
|
|
|
|
|
recipRows := sqlmock.NewRows([]string{"id"}).AddRow("ws-1")
|
|
|
|
|
mock.ExpectQuery("SELECT id FROM workspaces WHERE status != 'removed' AND id != $1").
|
|
|
|
|
WithArgs(broadcastTestUUID).
|
|
|
|
|
WillReturnRows(recipRows)
|
|
|
|
|
|
|
|
|
|
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, source_id, summary, status) VALUES ($1, 'broadcast_receive', 'broadcast', $2, $3, 'ok')").
|
|
|
|
|
WithArgs("ws-1", broadcastTestUUID, sqlmock.AnyArg()).
|
|
|
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
|
|
|
|
|
|
// Sender log fails — but handler still returns 200 (logged only).
|
|
|
|
|
mock.ExpectExec("INSERT INTO activity_logs (workspace_id, activity_type, method, summary, status) VALUES ($1, 'broadcast_sent', 'broadcast', $2, 'ok')").
|
|
|
|
|
WithArgs(broadcastTestUUID, sqlmock.AnyArg()).
|
|
|
|
|
WillReturnError(sql.ErrConnDone)
|
|
|
|
|
|
|
|
|
|
handler := NewBroadcastHandler(events.NewBroadcaster(ws.NewHub(nil)))
|
|
|
|
|
handler.Broadcast(c)
|
|
|
|
|
|
|
|
|
|
if w.Code != http.StatusOK {
|
|
|
|
|
t.Errorf("want 200 despite sender log error, got %d: %s", w.Code, w.Body.String())
|
|
|
|
|
}
|
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
|
|
|
t.Errorf("unmet mock expectations: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|