@@ -17,6 +17,7 @@ import (
1717 "github.com/getmockd/mockd/pkg/mock"
1818 "github.com/getmockd/mockd/pkg/requestlog"
1919 "github.com/getmockd/mockd/pkg/stateful"
20+ "github.com/getmockd/mockd/pkg/websocket"
2021)
2122
2223// mockEngine is a test double for EngineController.
@@ -49,13 +50,17 @@ type mockEngine struct {
4950 resetStateErr error
5051 listStatefulItemsErr error
5152 getStatefulItemErr error
53+ // wsSendErr allows injecting a specific error for a connection ID in
54+ // SendToWebSocketConnection. Keyed by connection ID.
55+ wsSendErr map [string ]error
5256}
5357
5458func newMockEngine () * mockEngine {
5559 return & mockEngine {
5660 mocks : make (map [string ]* config.MockConfiguration ),
5761 requestLogs : make (map [string ]* requestlog.Entry ),
5862 customOps : make (map [string ]* CustomOperationDetail ),
63+ wsSendErr : make (map [string ]error ),
5964 running : true ,
6065 uptime : 100 ,
6166 protocols : map [string ]ProtocolStatusInfo {
@@ -360,12 +365,16 @@ func (m *mockEngine) GetWebSocketStats() *WebSocketStats {
360365}
361366
362367func (m * mockEngine ) SendToWebSocketConnection (id string , msgType string , data []byte ) error {
368+ // Allow per-connection error injection for testing specific error paths.
369+ if err , ok := m .wsSendErr [id ]; ok {
370+ return err
371+ }
363372 for _ , c := range m .wsConnections {
364373 if c .ID == id {
365374 return nil
366375 }
367376 }
368- return errors . New ( "connection not found" )
377+ return websocket . ErrConnectionNotFound
369378}
370379
371380func (m * mockEngine ) GetConfig () * ConfigResponse {
@@ -2406,3 +2415,169 @@ func TestHandleGetWebSocketStats(t *testing.T) {
24062415 assert .Equal (t , 3 , stats .ConnectionsByMock ["mock-1" ])
24072416 })
24082417}
2418+
2419+ // TestHandleSendToWebSocketConnection covers every branch of the send handler,
2420+ // including the new 404/500 distinction introduced in the review fix.
2421+ func TestHandleSendToWebSocketConnection (t * testing.T ) {
2422+ t .Run ("returns 400 when connection id is missing" , func (t * testing.T ) {
2423+ engine := newMockEngine ()
2424+ server := newTestServer (engine )
2425+
2426+ req := httptest .NewRequest (http .MethodPost , "/websocket/connections//send" ,
2427+ strings .NewReader (`{"type":"text","data":"hello"}` ))
2428+ // PathValue "id" intentionally not set → empty string
2429+ rec := httptest .NewRecorder ()
2430+
2431+ server .handleSendToWebSocketConnection (rec , req )
2432+
2433+ assert .Equal (t , http .StatusBadRequest , rec .Code )
2434+ var resp map [string ]interface {}
2435+ require .NoError (t , json .Unmarshal (rec .Body .Bytes (), & resp ))
2436+ assert .Equal (t , "missing_id" , resp ["error" ])
2437+ })
2438+
2439+ t .Run ("returns 400 for invalid JSON body" , func (t * testing.T ) {
2440+ engine := newMockEngine ()
2441+ server := newTestServer (engine )
2442+
2443+ req := httptest .NewRequest (http .MethodPost , "/websocket/connections/ws-1/send" ,
2444+ strings .NewReader (`{invalid` ))
2445+ req .SetPathValue ("id" , "ws-1" )
2446+ rec := httptest .NewRecorder ()
2447+
2448+ server .handleSendToWebSocketConnection (rec , req )
2449+
2450+ assert .Equal (t , http .StatusBadRequest , rec .Code )
2451+ })
2452+
2453+ t .Run ("returns 400 for invalid base64 binary payload" , func (t * testing.T ) {
2454+ engine := newMockEngine ()
2455+ server := newTestServer (engine )
2456+ engine .wsConnections = []* WebSocketConnection {{ID : "ws-1" }}
2457+
2458+ req := httptest .NewRequest (http .MethodPost , "/websocket/connections/ws-1/send" ,
2459+ strings .NewReader (`{"type":"binary","data":"not-valid-base64!!!"}` ))
2460+ req .SetPathValue ("id" , "ws-1" )
2461+ rec := httptest .NewRecorder ()
2462+
2463+ server .handleSendToWebSocketConnection (rec , req )
2464+
2465+ assert .Equal (t , http .StatusBadRequest , rec .Code )
2466+ var resp map [string ]interface {}
2467+ require .NoError (t , json .Unmarshal (rec .Body .Bytes (), & resp ))
2468+ assert .Equal (t , "invalid_base64" , resp ["error" ])
2469+ })
2470+
2471+ t .Run ("returns 200 for text message to existing connection" , func (t * testing.T ) {
2472+ engine := newMockEngine ()
2473+ server := newTestServer (engine )
2474+ engine .wsConnections = []* WebSocketConnection {{ID : "ws-1" }}
2475+
2476+ req := httptest .NewRequest (http .MethodPost , "/websocket/connections/ws-1/send" ,
2477+ strings .NewReader (`{"type":"text","data":"hello"}` ))
2478+ req .SetPathValue ("id" , "ws-1" )
2479+ rec := httptest .NewRecorder ()
2480+
2481+ server .handleSendToWebSocketConnection (rec , req )
2482+
2483+ assert .Equal (t , http .StatusOK , rec .Code )
2484+ var resp map [string ]interface {}
2485+ require .NoError (t , json .Unmarshal (rec .Body .Bytes (), & resp ))
2486+ assert .Equal (t , "Message sent" , resp ["message" ])
2487+ assert .Equal (t , "ws-1" , resp ["connection" ])
2488+ assert .Equal (t , "text" , resp ["type" ])
2489+ })
2490+
2491+ t .Run ("returns 200 for valid base64 binary message" , func (t * testing.T ) {
2492+ engine := newMockEngine ()
2493+ server := newTestServer (engine )
2494+ engine .wsConnections = []* WebSocketConnection {{ID : "ws-1" }}
2495+
2496+ // base64("hello") = "aGVsbG8="
2497+ req := httptest .NewRequest (http .MethodPost , "/websocket/connections/ws-1/send" ,
2498+ strings .NewReader (`{"type":"binary","data":"aGVsbG8="}` ))
2499+ req .SetPathValue ("id" , "ws-1" )
2500+ rec := httptest .NewRecorder ()
2501+
2502+ server .handleSendToWebSocketConnection (rec , req )
2503+
2504+ assert .Equal (t , http .StatusOK , rec .Code )
2505+ var resp map [string ]interface {}
2506+ require .NoError (t , json .Unmarshal (rec .Body .Bytes (), & resp ))
2507+ assert .Equal (t , "binary" , resp ["type" ])
2508+ })
2509+
2510+ t .Run ("defaults type to text when omitted" , func (t * testing.T ) {
2511+ engine := newMockEngine ()
2512+ server := newTestServer (engine )
2513+ engine .wsConnections = []* WebSocketConnection {{ID : "ws-1" }}
2514+
2515+ req := httptest .NewRequest (http .MethodPost , "/websocket/connections/ws-1/send" ,
2516+ strings .NewReader (`{"data":"hello"}` ))
2517+ req .SetPathValue ("id" , "ws-1" )
2518+ rec := httptest .NewRecorder ()
2519+
2520+ server .handleSendToWebSocketConnection (rec , req )
2521+
2522+ assert .Equal (t , http .StatusOK , rec .Code )
2523+ var resp map [string ]interface {}
2524+ require .NoError (t , json .Unmarshal (rec .Body .Bytes (), & resp ))
2525+ assert .Equal (t , "text" , resp ["type" ])
2526+ })
2527+
2528+ t .Run ("returns 404 when connection is not found" , func (t * testing.T ) {
2529+ engine := newMockEngine ()
2530+ server := newTestServer (engine )
2531+ // No connections registered → ErrConnectionNotFound
2532+
2533+ req := httptest .NewRequest (http .MethodPost , "/websocket/connections/missing/send" ,
2534+ strings .NewReader (`{"type":"text","data":"hello"}` ))
2535+ req .SetPathValue ("id" , "missing" )
2536+ rec := httptest .NewRecorder ()
2537+
2538+ server .handleSendToWebSocketConnection (rec , req )
2539+
2540+ assert .Equal (t , http .StatusNotFound , rec .Code )
2541+ var resp map [string ]interface {}
2542+ require .NoError (t , json .Unmarshal (rec .Body .Bytes (), & resp ))
2543+ assert .Equal (t , "not_found" , resp ["error" ])
2544+ })
2545+
2546+ t .Run ("returns 404 when connection is already closed" , func (t * testing.T ) {
2547+ engine := newMockEngine ()
2548+ server := newTestServer (engine )
2549+ // Inject ErrConnectionClosed for ws-1 (connection exists but is closed)
2550+ engine .wsSendErr ["ws-1" ] = websocket .ErrConnectionClosed
2551+
2552+ req := httptest .NewRequest (http .MethodPost , "/websocket/connections/ws-1/send" ,
2553+ strings .NewReader (`{"type":"text","data":"hello"}` ))
2554+ req .SetPathValue ("id" , "ws-1" )
2555+ rec := httptest .NewRecorder ()
2556+
2557+ server .handleSendToWebSocketConnection (rec , req )
2558+
2559+ assert .Equal (t , http .StatusNotFound , rec .Code )
2560+ var resp map [string ]interface {}
2561+ require .NoError (t , json .Unmarshal (rec .Body .Bytes (), & resp ))
2562+ assert .Equal (t , "not_found" , resp ["error" ])
2563+ })
2564+
2565+ t .Run ("returns 500 for unexpected write error" , func (t * testing.T ) {
2566+ engine := newMockEngine ()
2567+ server := newTestServer (engine )
2568+ // Inject a generic I/O error (e.g., broken pipe)
2569+ engine .wsSendErr ["ws-1" ] = errors .New ("write: broken pipe" )
2570+
2571+ req := httptest .NewRequest (http .MethodPost , "/websocket/connections/ws-1/send" ,
2572+ strings .NewReader (`{"type":"text","data":"hello"}` ))
2573+ req .SetPathValue ("id" , "ws-1" )
2574+ rec := httptest .NewRecorder ()
2575+
2576+ server .handleSendToWebSocketConnection (rec , req )
2577+
2578+ assert .Equal (t , http .StatusInternalServerError , rec .Code )
2579+ var resp map [string ]interface {}
2580+ require .NoError (t , json .Unmarshal (rec .Body .Bytes (), & resp ))
2581+ assert .Equal (t , "send_error" , resp ["error" ])
2582+ })
2583+ }
0 commit comments