Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee9a1dca73 |
@@ -0,0 +1,315 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------------------- //
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------- //
|
||||
|
||||
func setupAbilitiesTest(t *testing.T) (sqlmock.Sqlmock, func()) {
|
||||
t.Helper()
|
||||
mockDB, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create sqlmock: %v", err)
|
||||
}
|
||||
prev := db.DB
|
||||
db.DB = mockDB
|
||||
return mock, func() {
|
||||
db.DB = prev
|
||||
mockDB.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------- //
|
||||
// PatchAbilities
|
||||
// -------------------------------------------------------------------------- //
|
||||
|
||||
func TestPatchAbilities_InvalidWorkspaceID_Returns400(t *testing.T) {
|
||||
_, cleanup := setupAbilitiesTest(t)
|
||||
defer cleanup()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "not-a-valid-uuid"}}
|
||||
c.Request = httptest.NewRequest("PATCH",
|
||||
"/workspaces/not-a-valid-uuid/abilities",
|
||||
bytes.NewBufferString(`{"broadcast_enabled":true}`))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
c.Request = c.Request.WithContext(context.Background())
|
||||
|
||||
PatchAbilities(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var body map[string]string
|
||||
json.Unmarshal(w.Body.Bytes(), &body)
|
||||
if body["error"] != "invalid workspace ID" {
|
||||
t.Errorf("expected 'invalid workspace ID', got %q", body["error"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchAbilities_EmptyBody_Returns400(t *testing.T) {
|
||||
_, cleanup := setupAbilitiesTest(t)
|
||||
defer cleanup()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}}
|
||||
c.Request = httptest.NewRequest("PATCH",
|
||||
"/workspaces/550e8400-e29b-41d4-a716-446655440000/abilities",
|
||||
bytes.NewBufferString(`{}`))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
c.Request = c.Request.WithContext(context.Background())
|
||||
|
||||
PatchAbilities(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var body map[string]string
|
||||
json.Unmarshal(w.Body.Bytes(), &body)
|
||||
if body["error"] != "at least one ability field required" {
|
||||
t.Errorf("expected 'at least one ability field required', got %q", body["error"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchAbilities_InvalidJSON_Returns400(t *testing.T) {
|
||||
_, cleanup := setupAbilitiesTest(t)
|
||||
defer cleanup()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}}
|
||||
c.Request = httptest.NewRequest("PATCH",
|
||||
"/workspaces/550e8400-e29b-41d4-a716-446655440000/abilities",
|
||||
bytes.NewBufferString(`{invalid json}`))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
c.Request = c.Request.WithContext(context.Background())
|
||||
|
||||
PatchAbilities(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var body map[string]string
|
||||
json.Unmarshal(w.Body.Bytes(), &body)
|
||||
if body["error"] != "invalid request body" {
|
||||
t.Errorf("expected 'invalid request body', got %q", body["error"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchAbilities_WorkspaceNotFound_Returns404(t *testing.T) {
|
||||
mock, cleanup := setupAbilitiesTest(t)
|
||||
defer cleanup()
|
||||
|
||||
mock.ExpectQuery("SELECT EXISTS").
|
||||
WithArgs("550e8400-e29b-41d4-a716-446655440000").
|
||||
WillReturnError(sql.ErrNoRows)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}}
|
||||
c.Request = httptest.NewRequest("PATCH",
|
||||
"/workspaces/550e8400-e29b-41d4-a716-446655440000/abilities",
|
||||
bytes.NewBufferString(`{"broadcast_enabled":true}`))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
c.Request = c.Request.WithContext(context.Background())
|
||||
|
||||
PatchAbilities(c)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var body map[string]string
|
||||
json.Unmarshal(w.Body.Bytes(), &body)
|
||||
if body["error"] != "workspace not found" {
|
||||
t.Errorf("expected 'workspace not found', got %q", body["error"])
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchAbilities_WorkspaceDBError_Returns500(t *testing.T) {
|
||||
mock, cleanup := setupAbilitiesTest(t)
|
||||
defer cleanup()
|
||||
|
||||
mock.ExpectQuery("SELECT EXISTS").
|
||||
WithArgs("550e8400-e29b-41d4-a716-446655440000").
|
||||
WillReturnError(errors.New("connection refused"))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}}
|
||||
c.Request = httptest.NewRequest("PATCH",
|
||||
"/workspaces/550e8400-e29b-41d4-a716-446655440000/abilities",
|
||||
bytes.NewBufferString(`{"broadcast_enabled":true}`))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
c.Request = c.Request.WithContext(context.Background())
|
||||
|
||||
PatchAbilities(c)
|
||||
|
||||
// Handler treats DB error as not-found (|| !exists short-circuits on err=true).
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected 404, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchAbilities_UpdateBroadcastEnabled_Returns200(t *testing.T) {
|
||||
mock, cleanup := setupAbilitiesTest(t)
|
||||
defer cleanup()
|
||||
|
||||
mock.ExpectQuery("SELECT EXISTS").
|
||||
WithArgs("550e8400-e29b-41d4-a716-446655440000").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
|
||||
// $1=id, $2=value in the UPDATE SET col=$2 WHERE id=$1 query.
|
||||
mock.ExpectExec("UPDATE workspaces SET broadcast_enabled").
|
||||
WithArgs("550e8400-e29b-41d4-a716-446655440000", true).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}}
|
||||
c.Request = httptest.NewRequest("PATCH",
|
||||
"/workspaces/550e8400-e29b-41d4-a716-446655440000/abilities",
|
||||
bytes.NewBufferString(`{"broadcast_enabled":true}`))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
c.Request = c.Request.WithContext(context.Background())
|
||||
|
||||
PatchAbilities(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var body map[string]string
|
||||
json.Unmarshal(w.Body.Bytes(), &body)
|
||||
if body["status"] != "updated" {
|
||||
t.Errorf("expected status=updated, got %v", body)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchAbilities_UpdateTalkToUserEnabled_Returns200(t *testing.T) {
|
||||
mock, cleanup := setupAbilitiesTest(t)
|
||||
defer cleanup()
|
||||
|
||||
mock.ExpectQuery("SELECT EXISTS").
|
||||
WithArgs("550e8400-e29b-41d4-a716-446655440000").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
|
||||
// $1=id, $2=value in the UPDATE SET col=$2 WHERE id=$1 query.
|
||||
mock.ExpectExec("UPDATE workspaces SET talk_to_user_enabled").
|
||||
WithArgs("550e8400-e29b-41d4-a716-446655440000", true).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}}
|
||||
c.Request = httptest.NewRequest("PATCH",
|
||||
"/workspaces/550e8400-e29b-41d4-a716-446655440000/abilities",
|
||||
bytes.NewBufferString(`{"talk_to_user_enabled":true}`))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
c.Request = c.Request.WithContext(context.Background())
|
||||
|
||||
PatchAbilities(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var body map[string]string
|
||||
json.Unmarshal(w.Body.Bytes(), &body)
|
||||
if body["status"] != "updated" {
|
||||
t.Errorf("expected status=updated, got %v", body)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchAbilities_UpdateBothAbilities_Returns200(t *testing.T) {
|
||||
mock, cleanup := setupAbilitiesTest(t)
|
||||
defer cleanup()
|
||||
|
||||
mock.ExpectQuery("SELECT EXISTS").
|
||||
WithArgs("550e8400-e29b-41d4-a716-446655440000").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
|
||||
// $1=id, $2=value in the UPDATE SET col=$2 WHERE id=$1 query.
|
||||
mock.ExpectExec("UPDATE workspaces SET broadcast_enabled").
|
||||
WithArgs("550e8400-e29b-41d4-a716-446655440000", true).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectExec("UPDATE workspaces SET talk_to_user_enabled").
|
||||
WithArgs("550e8400-e29b-41d4-a716-446655440000", false).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}}
|
||||
c.Request = httptest.NewRequest("PATCH",
|
||||
"/workspaces/550e8400-e29b-41d4-a716-446655440000/abilities",
|
||||
bytes.NewBufferString(`{"broadcast_enabled":true,"talk_to_user_enabled":false}`))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
c.Request = c.Request.WithContext(context.Background())
|
||||
|
||||
PatchAbilities(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
var body map[string]string
|
||||
json.Unmarshal(w.Body.Bytes(), &body)
|
||||
if body["status"] != "updated" {
|
||||
t.Errorf("expected status=updated, got %v", body)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchAbilities_UpdateBroadcastDisabled_Returns200(t *testing.T) {
|
||||
mock, cleanup := setupAbilitiesTest(t)
|
||||
defer cleanup()
|
||||
|
||||
mock.ExpectQuery("SELECT EXISTS").
|
||||
WithArgs("550e8400-e29b-41d4-a716-446655440000").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
|
||||
// $1=id, $2=value in the UPDATE SET col=$2 WHERE id=$1 query.
|
||||
mock.ExpectExec("UPDATE workspaces SET broadcast_enabled").
|
||||
WithArgs("550e8400-e29b-41d4-a716-446655440000", false).
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "550e8400-e29b-41d4-a716-446655440000"}}
|
||||
c.Request = httptest.NewRequest("PATCH",
|
||||
"/workspaces/550e8400-e29b-41d4-a716-446655440000/abilities",
|
||||
bytes.NewBufferString(`{"broadcast_enabled":false}`))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
c.Request = c.Request.WithContext(context.Background())
|
||||
|
||||
PatchAbilities(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("unmet expectations: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
package handlers
|
||||
|
||||
// workspace_broadcast_test.go — coverage for workspace_broadcast.go.
|
||||
//
|
||||
// Covered handlers:
|
||||
// - BroadcastHandler.Broadcast POST /workspaces/:id/broadcast
|
||||
// - broadcastTruncate pure function
|
||||
//
|
||||
// DB reads are mocked via sqlmock. The *events.Broadcaster is injected
|
||||
// as the real no-op test broadcaster so BroadcastOnly() is safe in tests.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ─── broadcastTruncate ─────────────────────────────────────────────────────────
|
||||
|
||||
func TestBroadcastTruncate_LenBelowMax_ReturnsFullString(t *testing.T) {
|
||||
result := broadcastTruncate("hello", 10)
|
||||
require.Equal(t, "hello", result)
|
||||
}
|
||||
|
||||
func TestBroadcastTruncate_LenEqualMax_ReturnsFullString(t *testing.T) {
|
||||
result := broadcastTruncate("hello", 5)
|
||||
require.Equal(t, "hello", result)
|
||||
}
|
||||
|
||||
func TestBroadcastTruncate_LenAboveMax_TruncatesWithEllipsis(t *testing.T) {
|
||||
result := broadcastTruncate("hello world", 5)
|
||||
require.Equal(t, "hello…", result)
|
||||
}
|
||||
|
||||
func TestBroadcastTruncate_EmptyString_ReturnsEmpty(t *testing.T) {
|
||||
result := broadcastTruncate("", 5)
|
||||
require.Equal(t, "", result)
|
||||
}
|
||||
|
||||
func TestBroadcastTruncate_Unicode_TruncatesAtRuneBoundary(t *testing.T) {
|
||||
// "日本語" is 3 runes; truncating at max=2 should give 2 runes + ellipsis.
|
||||
result := broadcastTruncate("日本語abcdef", 2)
|
||||
require.Equal(t, "日本…", result)
|
||||
}
|
||||
|
||||
// ─── Broadcast handler ────────────────────────────────────────────────────────
|
||||
|
||||
// Valid UUIDs used throughout the test suite.
|
||||
const (
|
||||
testSenderID = "00000000-0000-0000-0000-000000000001"
|
||||
testRecipient1 = "00000000-0000-0000-0000-000000000002"
|
||||
testRecipient2 = "00000000-0000-0000-0000-000000000003"
|
||||
)
|
||||
|
||||
func setupBroadcastCtx(t *testing.T, body string) (*BroadcastHandler, sqlmock.Sqlmock, *httptest.ResponseRecorder, *gin.Context) {
|
||||
t.Helper()
|
||||
mockDB, mock, err := sqlmock.New()
|
||||
require.NoError(t, err)
|
||||
prevDB := db.DB
|
||||
db.DB = mockDB
|
||||
t.Cleanup(func() { db.DB = prevDB; mockDB.Close() })
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: testSenderID}}
|
||||
c.Request = httptest.NewRequest("POST", "/workspaces/"+testSenderID+"/broadcast", strings.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
h := NewBroadcastHandler(newTestBroadcaster())
|
||||
return h, mock, w, c
|
||||
}
|
||||
|
||||
func TestBroadcast_InvalidWorkspaceID_Returns400(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("POST", "/workspaces/not-a-uuid/broadcast", nil)
|
||||
c.Params = gin.Params{{Key: "id", Value: "not-a-uuid"}}
|
||||
|
||||
h := NewBroadcastHandler(newTestBroadcaster())
|
||||
h.Broadcast(c)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
var body map[string]string
|
||||
json.Unmarshal(w.Body.Bytes(), &body)
|
||||
require.Contains(t, body["error"], "invalid workspace ID")
|
||||
}
|
||||
|
||||
func TestBroadcast_MissingMessage_Returns400(t *testing.T) {
|
||||
h, _, w, c := setupBroadcastCtx(t, `{}`)
|
||||
|
||||
// ShouldBindJSON fails first — no DB query expected.
|
||||
h.Broadcast(c)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
var body map[string]string
|
||||
json.Unmarshal(w.Body.Bytes(), &body)
|
||||
require.Equal(t, "message is required", body["error"])
|
||||
}
|
||||
|
||||
func TestBroadcast_WorkspaceNotFound_Returns404(t *testing.T) {
|
||||
h, mock, w, c := setupBroadcastCtx(t, `{"message":"hello"}`)
|
||||
|
||||
mock.ExpectQuery(`SELECT name, broadcast_enabled FROM workspaces WHERE id = \$1 AND status != 'removed'`).
|
||||
WithArgs(testSenderID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"name", "broadcast_enabled"})) // empty
|
||||
|
||||
h.Broadcast(c)
|
||||
|
||||
require.Equal(t, http.StatusNotFound, w.Code)
|
||||
var body map[string]string
|
||||
json.Unmarshal(w.Body.Bytes(), &body)
|
||||
require.Equal(t, "workspace not found", body["error"])
|
||||
}
|
||||
|
||||
func TestBroadcast_BroadcastDisabled_Returns403(t *testing.T) {
|
||||
h, mock, w, c := setupBroadcastCtx(t, `{"message":"hello"}`)
|
||||
|
||||
mock.ExpectQuery(`SELECT name, broadcast_enabled FROM workspaces WHERE id = \$1 AND status != 'removed'`).
|
||||
WithArgs(testSenderID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
|
||||
AddRow("test-ws", false))
|
||||
|
||||
h.Broadcast(c)
|
||||
|
||||
require.Equal(t, http.StatusForbidden, w.Code)
|
||||
var body map[string]string
|
||||
json.Unmarshal(w.Body.Bytes(), &body)
|
||||
require.Equal(t, "broadcast_disabled", body["error"])
|
||||
}
|
||||
|
||||
func TestBroadcast_RecipientQueryError_Returns500(t *testing.T) {
|
||||
h, mock, w, c := setupBroadcastCtx(t, `{"message":"hello"}`)
|
||||
|
||||
mock.ExpectQuery(`SELECT name, broadcast_enabled FROM workspaces WHERE id = \$1 AND status != 'removed'`).
|
||||
WithArgs(testSenderID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
|
||||
AddRow("test-ws", true))
|
||||
mock.ExpectQuery(`SELECT id FROM workspaces WHERE status != 'removed' AND id != \$1`).
|
||||
WithArgs(testSenderID).
|
||||
WillReturnError(context.DeadlineExceeded)
|
||||
|
||||
h.Broadcast(c)
|
||||
|
||||
require.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
|
||||
func TestBroadcast_Success_Returns200AndDeliveredCount(t *testing.T) {
|
||||
h, mock, w, c := setupBroadcastCtx(t, `{"message":"hello world"}`)
|
||||
|
||||
mock.ExpectQuery(`SELECT name, broadcast_enabled FROM workspaces WHERE id = \$1 AND status != 'removed'`).
|
||||
WithArgs(testSenderID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
|
||||
AddRow("test-ws", true))
|
||||
// Two recipients.
|
||||
mock.ExpectQuery(`SELECT id FROM workspaces WHERE status != 'removed' AND id != \$1`).
|
||||
WithArgs(testSenderID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(testRecipient1).AddRow(testRecipient2))
|
||||
// Activity log insert per recipient.
|
||||
mock.ExpectExec(`INSERT INTO activity_logs`).
|
||||
WithArgs(testRecipient1, testSenderID, "Broadcast from test-ws: hello world").
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectExec(`INSERT INTO activity_logs`).
|
||||
WithArgs(testRecipient2, testSenderID, "Broadcast from test-ws: hello world").
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
// Sender's own log.
|
||||
mock.ExpectExec(`INSERT INTO activity_logs`).
|
||||
WithArgs(testSenderID, "Broadcast sent to 2 workspace(s)").
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
h.Broadcast(c)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
var body map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &body)
|
||||
require.Equal(t, "sent", body["status"])
|
||||
require.Equal(t, float64(2), body["delivered"])
|
||||
}
|
||||
|
||||
func TestBroadcast_NoRecipients_ReturnsZeroDelivered(t *testing.T) {
|
||||
h, mock, w, c := setupBroadcastCtx(t, `{"message":"hello"}`)
|
||||
|
||||
mock.ExpectQuery(`SELECT name, broadcast_enabled FROM workspaces WHERE id = \$1 AND status != 'removed'`).
|
||||
WithArgs(testSenderID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
|
||||
AddRow("solo-ws", true))
|
||||
// No other workspaces.
|
||||
mock.ExpectQuery(`SELECT id FROM workspaces WHERE status != 'removed' AND id != \$1`).
|
||||
WithArgs(testSenderID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}))
|
||||
// Sender log still fires.
|
||||
mock.ExpectExec(`INSERT INTO activity_logs`).
|
||||
WithArgs(testSenderID, "Broadcast sent to 0 workspace(s)").
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
h.Broadcast(c)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
var body map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &body)
|
||||
require.Equal(t, "sent", body["status"])
|
||||
require.Equal(t, float64(0), body["delivered"])
|
||||
}
|
||||
|
||||
func TestBroadcast_ActivityLogInsertFails_StillReturns200(t *testing.T) {
|
||||
// Sender's own activity log is best-effort; a DB error is logged but
|
||||
// does NOT fail the HTTP response.
|
||||
h, mock, w, c := setupBroadcastCtx(t, `{"message":"hello"}`)
|
||||
|
||||
mock.ExpectQuery(`SELECT name, broadcast_enabled FROM workspaces WHERE id = \$1 AND status != 'removed'`).
|
||||
WithArgs(testSenderID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
|
||||
AddRow("test-ws", true))
|
||||
mock.ExpectQuery(`SELECT id FROM workspaces WHERE status != 'removed' AND id != \$1`).
|
||||
WithArgs(testSenderID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}))
|
||||
// Recipient insert succeeds.
|
||||
mock.ExpectExec(`INSERT INTO activity_logs`).
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
// Sender log FAILS — handler logs but still returns 200.
|
||||
mock.ExpectExec(`INSERT INTO activity_logs`).
|
||||
WillReturnError(context.DeadlineExceeded)
|
||||
|
||||
h.Broadcast(c)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code) // NOT 500
|
||||
}
|
||||
|
||||
func TestBroadcast_RecipientInsertFails_ContinuesAndCountsOthers(t *testing.T) {
|
||||
// A recipient-level insert failure is logged; the handler continues
|
||||
// delivering to remaining recipients and reports the delivered count.
|
||||
h, mock, w, c := setupBroadcastCtx(t, `{"message":"hello"}`)
|
||||
|
||||
mock.ExpectQuery(`SELECT name, broadcast_enabled FROM workspaces WHERE id = \$1 AND status != 'removed'`).
|
||||
WithArgs(testSenderID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
|
||||
AddRow("test-ws", true))
|
||||
// Two recipients.
|
||||
mock.ExpectQuery(`SELECT id FROM workspaces WHERE status != 'removed' AND id != \$1`).
|
||||
WithArgs(testSenderID).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(testRecipient1).AddRow(testRecipient2))
|
||||
// testRecipient1 insert FAILS — logged, handler continues.
|
||||
mock.ExpectExec(`INSERT INTO activity_logs`).
|
||||
WithArgs(testRecipient1, testSenderID, "Broadcast from test-ws: hello").
|
||||
WillReturnError(context.DeadlineExceeded)
|
||||
// testRecipient2 insert succeeds.
|
||||
mock.ExpectExec(`INSERT INTO activity_logs`).
|
||||
WithArgs(testRecipient2, testSenderID, "Broadcast from test-ws: hello").
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
// Sender log.
|
||||
mock.ExpectExec(`INSERT INTO activity_logs`).
|
||||
WithArgs(testSenderID, "Broadcast sent to 1 workspace(s)").
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
h.Broadcast(c)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
var body map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &body)
|
||||
require.Equal(t, float64(1), body["delivered"]) // only testRecipient2 counted
|
||||
}
|
||||
|
||||
func TestBroadcast_NewBroadcastHandler(t *testing.T) {
|
||||
b := newTestBroadcaster()
|
||||
h := NewBroadcastHandler(b)
|
||||
require.NotNil(t, h)
|
||||
require.Equal(t, b, h.broadcaster)
|
||||
}
|
||||
Reference in New Issue
Block a user