From 278ebaa0870806178ee551f26d45df6378533aef Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Sun, 10 May 2026 21:08:41 -0400 Subject: [PATCH] webrtc: add 409 single-publisher enforcement test and SetMetrics/PublisherCount tests (issues #22, #26) --- app/webrtc/whip_handler_test.go | 67 ++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/app/webrtc/whip_handler_test.go b/app/webrtc/whip_handler_test.go index d3b3482..1c1e476 100644 --- a/app/webrtc/whip_handler_test.go +++ b/app/webrtc/whip_handler_test.go @@ -7,6 +7,8 @@ import ( "testing" "github.com/labstack/echo/v4" + + corewebrtc "github.com/datarhei/core/v16/core/webrtc" ) // TestWHIPHandler_Publish_404WhenNoIngest verifies POST /whip/:id returns @@ -81,6 +83,52 @@ func TestWHIPHandler_Publish_400OnNonSDP(t *testing.T) { } } +// TestWHIPHandler_Publish_409OnSecondPublisher verifies that attempting to +// publish a second time on the same stream while a publisher is already +// active returns 409 Conflict, not 201, and does not increment the counter. +func TestWHIPHandler_Publish_409OnSecondPublisher(t *testing.T) { + sub := newTestSubsystem(t) + sub.mu.Lock() + sub.whipIngests["probe"] = &ingestStream{id: "probe", videoPort: 5104, audioPort: 5105} + sub.mu.Unlock() + + h := NewWHIPHandler(sub, 0) + + // Inject a fake active publisher directly into the handler's index. + // We use a nil *IngestPeer because the 409 check only tests map length + // and never dereferences the peer pointer. + h.mu.Lock() + h.ingestByStream["probe"] = map[string]*corewebrtc.IngestPeer{ + "existing-rid": nil, + } + h.ingestStream["existing-rid"] = "probe" + h.mu.Unlock() + + // Verify initial count is 0 (the fake was injected, not published). + if c := h.PublisherCount(); c != 0 { + t.Fatalf("expected initial count 0, got %d", c) + } + + e := echo.New() + req := httptest.NewRequest(http.MethodPost, "/whip/probe", + strings.NewReader("v=0\r\nm=video 0 RTP/AVP 96\r\n")) + rec := httptest.NewRecorder() + ctx := e.NewContext(req, rec) + ctx.SetParamNames("id") + ctx.SetParamValues("probe") + + if err := h.Publish(ctx); err != nil { + t.Fatalf("Publish returned error: %v", err) + } + if rec.Code != http.StatusConflict { + t.Fatalf("expected 409, got %d: %s", rec.Code, rec.Body.String()) + } + // Count must not have incremented on the rejected request. + if c := h.PublisherCount(); c != 0 { + t.Errorf("expected count still 0 after 409, got %d", c) + } +} + // TestWHIPHandler_Unpublish_204WhenUnknown verifies DELETE returns 204 // even for unknown resource ids — idempotent per the WHIP spec. func TestWHIPHandler_Unpublish_204WhenUnknown(t *testing.T) { @@ -192,9 +240,26 @@ func TestWHIPHandler_Preflight_ExposesLinkHeader(t *testing.T) { } } +// TestWHIPHandler_SetMetrics_DoesNotPanic verifies that SetMetrics accepts +// a nil argument without panicking (nil-safe guard for wiring code). +func TestWHIPHandler_SetMetrics_DoesNotPanic(t *testing.T) { + h := NewWHIPHandler(newTestSubsystem(t), 0) + // nil metrics is explicitly allowed — recordRequest guards on h.met == nil. + h.SetMetrics(nil) +} + +// TestWHIPHandler_PublisherCount_ZeroOnEmpty verifies that a freshly +// constructed handler reports 0 active publishers. +func TestWHIPHandler_PublisherCount_ZeroOnEmpty(t *testing.T) { + h := NewWHIPHandler(newTestSubsystem(t), 0) + if n := h.PublisherCount(); n != 0 { + t.Errorf("expected 0 publishers on empty handler, got %d", n) + } +} + // TestWHIPHandler_Publish_CORSHeadersPresent verifies that every Publish // response (even a 404) carries the CORS headers required for cross-origin -// browser-based publishers (e.g., a browser-based OBS alternative). +// browser-based publishers. func TestWHIPHandler_Publish_CORSHeadersPresent(t *testing.T) { h := NewWHIPHandler(newTestSubsystem(t), 0)