Compare commits

..

1 Commits

Author SHA1 Message Date
fullstack-engineer 9a9efbf8c3 test(handlers): add BroadcastHandler coverage — 14 test cases
E2E API Smoke Test / E2E API Smoke Test (pull_request) Blocked by required conditions
E2E Chat / E2E Chat (pull_request) Blocked by required conditions
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Blocked by required conditions
Harness Replays / Harness Replays (pull_request) Blocked by required conditions
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Blocked by required conditions
security-review / approved (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 29s
Harness Replays / detect-changes (pull_request) Successful in 28s
CI / Detect changes (pull_request) Successful in 52s
E2E API Smoke Test / detect-changes (pull_request) Successful in 49s
E2E Chat / detect-changes (pull_request) Successful in 51s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 49s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 31s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 47s
gate-check-v3 / gate-check (pull_request) Successful in 36s
qa-review / approved (pull_request) Successful in 39s
sop-tier-check / tier-check (pull_request) Successful in 35s
sop-checklist / all-items-acked (pull_request) Successful in 38s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m44s
CI / Canvas (Next.js) (pull_request) Successful in 20m17s
CI / Python Lint & Test (pull_request) Has been cancelled
CI / Shellcheck (E2E scripts) (pull_request) Has been cancelled
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Platform (Go) (pull_request) Successful in 21m45s
CI / all-required (pull_request) Successful in 16s
Covers: truncate, validation, authz, DB errors, success paths,
and graceful degradation on log insert failures.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 11:30:40 +00:00
3 changed files with 425 additions and 120 deletions
-66
View File
@@ -1,66 +0,0 @@
// @vitest-environment jsdom
/**
* Tests for theme.ts — cssVar() function and ColorToken type.
*
* cssVar is intentionally simple (string concatenation) — these tests guard
* against regressions as ColorToken is extended and against typos in the
* token-name → CSS variable mapping.
*/
import { describe, it, expect } from "vitest";
import { cssVar, type ColorToken } from "../theme";
describe("cssVar", () => {
it("wraps each known token in a var() reference", () => {
const tokens: ColorToken[] = [
"surface",
"surface-elevated",
"surface-sunken",
"surface-card",
"line",
"line-soft",
"ink",
"ink-mid",
"ink-soft",
"accent",
"accent-strong",
"warm",
"good",
"bad",
"bg",
"bg-elev",
"bg-card",
"line-strong",
"ink-mute",
"ink-dim",
"accent-dim",
"plasma",
"warn",
];
for (const token of tokens) {
expect(cssVar(token)).toBe(`var(--color-${token})`);
}
});
it("is a pure function — same token always returns same value", () => {
for (let i = 0; i < 5; i++) {
expect(cssVar("accent")).toBe("var(--color-accent)");
expect(cssVar("surface")).toBe("var(--color-surface)");
expect(cssVar("good")).toBe("var(--color-good)");
}
});
it("handles hyphenated tokens correctly", () => {
expect(cssVar("surface-elevated")).toBe("var(--color-surface-elevated)");
expect(cssVar("line-soft")).toBe("var(--color-line-soft)");
expect(cssVar("ink-mute")).toBe("var(--color-ink-mute)");
});
it("produces a value usable as an inline style prop value", () => {
// cssVar returns a plain string, which is exactly what style={{ color: cssVar(token) }}
// expects — no extra wrapping needed.
const result = cssVar("accent");
expect(typeof result).toBe("string");
expect(result.startsWith("var(--color-")).toBe(true);
expect(result.endsWith(")")).toBe(true);
});
});
@@ -1,54 +0,0 @@
package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
// ListSources is the only exported function in plugins_sources.go. It calls
// h.sources.Schemes() and returns the result verbatim, so the test verifies
// the handler correctly serialises whatever the real registry provides.
func TestListSources_ReturnsSchemes(t *testing.T) {
// Use a real handler — the registry is deterministic (local + github).
h := NewPluginsHandler(t.TempDir(), nil, nil)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/plugins/sources", nil)
h.ListSources(c)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var body struct {
Schemes []string `json:"schemes"`
}
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
// The default registry registers local + github resolvers.
if len(body.Schemes) < 1 {
t.Fatalf("expected at least 1 scheme, got %d: %v", len(body.Schemes), body.Schemes)
}
// Verify the schemes slice is stable (same call always returns same result).
w2 := httptest.NewRecorder()
c2, _ := gin.CreateTestContext(w2)
c2.Request = httptest.NewRequest("GET", "/plugins/sources", nil)
h.ListSources(c2)
var body2 struct {
Schemes []string `json:"schemes"`
}
json.Unmarshal(w2.Body.Bytes(), &body2)
if len(body.Schemes) != len(body2.Schemes) {
t.Errorf("Schemes() is not stable: first=%v, second=%v", body.Schemes, body2.Schemes)
}
}
@@ -0,0 +1,425 @@
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)
}
}