webrtc: add whip_handler_test.go — Publish/Unpublish/TrickleIngest/CORS tests (issue #21)
This commit is contained in:
parent
f1062a4c36
commit
1648deccf4
1 changed files with 213 additions and 0 deletions
213
app/webrtc/whip_handler_test.go
Normal file
213
app/webrtc/whip_handler_test.go
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
package webrtc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// TestWHIPHandler_Publish_404WhenNoIngest verifies POST /whip/:id returns
|
||||
// 404 when no process has registered a WHIP ingest for that id.
|
||||
func TestWHIPHandler_Publish_404WhenNoIngest(t *testing.T) {
|
||||
h := NewWHIPHandler(newTestSubsystem(t), 0)
|
||||
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(http.MethodPost, "/whip/ghost", strings.NewReader("v=0\r\n"))
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
c.SetParamNames("id")
|
||||
c.SetParamValues("ghost")
|
||||
|
||||
if err := h.Publish(c); err != nil {
|
||||
t.Fatalf("Publish returned error: %v", err)
|
||||
}
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Fatalf("expected 404, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestWHIPHandler_Publish_400OnEmptyBody verifies that an empty SDP body
|
||||
// is rejected before any peer negotiation. A dummy ingest is registered so
|
||||
// the handler reaches body validation.
|
||||
func TestWHIPHandler_Publish_400OnEmptyBody(t *testing.T) {
|
||||
sub := newTestSubsystem(t)
|
||||
sub.mu.Lock()
|
||||
sub.whipIngests["probe"] = &ingestStream{id: "probe", videoPort: 5100, audioPort: 5101}
|
||||
sub.mu.Unlock()
|
||||
|
||||
h := NewWHIPHandler(sub, 0)
|
||||
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(http.MethodPost, "/whip/probe", strings.NewReader(""))
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
c.SetParamNames("id")
|
||||
c.SetParamValues("probe")
|
||||
|
||||
if err := h.Publish(c); err != nil {
|
||||
t.Fatalf("Publish returned error: %v", err)
|
||||
}
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestWHIPHandler_Publish_400OnNonSDP verifies that a body which doesn't
|
||||
// start with "v=" is rejected as an invalid SDP.
|
||||
func TestWHIPHandler_Publish_400OnNonSDP(t *testing.T) {
|
||||
sub := newTestSubsystem(t)
|
||||
sub.mu.Lock()
|
||||
sub.whipIngests["probe"] = &ingestStream{id: "probe", videoPort: 5102, audioPort: 5103}
|
||||
sub.mu.Unlock()
|
||||
|
||||
h := NewWHIPHandler(sub, 0)
|
||||
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(http.MethodPost, "/whip/probe",
|
||||
strings.NewReader("not-an-sdp-body"))
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
c.SetParamNames("id")
|
||||
c.SetParamValues("probe")
|
||||
|
||||
if err := h.Publish(c); err != nil {
|
||||
t.Fatalf("Publish returned error: %v", err)
|
||||
}
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestWHIPHandler_Unpublish_204WhenUnknown verifies DELETE returns 204
|
||||
// even for unknown resource ids — idempotent per the WHIP spec.
|
||||
func TestWHIPHandler_Unpublish_204WhenUnknown(t *testing.T) {
|
||||
h := NewWHIPHandler(newTestSubsystem(t), 0)
|
||||
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(http.MethodDelete, "/whip/id/unknown-resource", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
c.SetParamNames("id", "resource")
|
||||
c.SetParamValues("id", "unknown-resource")
|
||||
|
||||
if err := h.Unpublish(c); err != nil {
|
||||
t.Fatalf("Unpublish returned error: %v", err)
|
||||
}
|
||||
if rec.Code != http.StatusNoContent {
|
||||
t.Fatalf("expected 204, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWHIPHandler_Unpublish_400OnMissingResource verifies DELETE without
|
||||
// a resource id param returns 400.
|
||||
func TestWHIPHandler_Unpublish_400OnMissingResource(t *testing.T) {
|
||||
h := NewWHIPHandler(newTestSubsystem(t), 0)
|
||||
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(http.MethodDelete, "/whip/id/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
c.SetParamNames("id", "resource")
|
||||
c.SetParamValues("id", "")
|
||||
|
||||
if err := h.Unpublish(c); err != nil {
|
||||
t.Fatalf("Unpublish returned error: %v", err)
|
||||
}
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWHIPHandler_TrickleIngest_404WhenPeerUnknown verifies PATCH returns
|
||||
// 404 when there is no peer registered for the resource id.
|
||||
func TestWHIPHandler_TrickleIngest_404WhenPeerUnknown(t *testing.T) {
|
||||
h := NewWHIPHandler(newTestSubsystem(t), 0)
|
||||
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(http.MethodPatch, "/whip/id/ghost",
|
||||
strings.NewReader("a=candidate:1 1 UDP 2122252543 192.168.1.1 12345 typ host\r\n"))
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
c.SetParamNames("id", "resource")
|
||||
c.SetParamValues("id", "ghost")
|
||||
|
||||
if err := h.TrickleIngest(c); err != nil {
|
||||
t.Fatalf("TrickleIngest returned error: %v", err)
|
||||
}
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Fatalf("expected 404, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWHIPHandler_TrickleIngest_400OnMissingResource verifies PATCH
|
||||
// without a resource id returns 400.
|
||||
func TestWHIPHandler_TrickleIngest_400OnMissingResource(t *testing.T) {
|
||||
h := NewWHIPHandler(newTestSubsystem(t), 0)
|
||||
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(http.MethodPatch, "/whip/id/",
|
||||
strings.NewReader("a=candidate:1 1 UDP 2122252543 192.168.1.1 12345 typ host\r\n"))
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
c.SetParamNames("id", "resource")
|
||||
c.SetParamValues("id", "")
|
||||
|
||||
if err := h.TrickleIngest(c); err != nil {
|
||||
t.Fatalf("TrickleIngest returned error: %v", err)
|
||||
}
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWHIPHandler_Preflight_ExposesLinkHeader verifies that CORS preflight
|
||||
// responses include "Link" in Access-Control-Expose-Headers so browsers
|
||||
// can read the RFC 9261 §5.2 Link headers on the 201 Publish response.
|
||||
func TestWHIPHandler_Preflight_ExposesLinkHeader(t *testing.T) {
|
||||
h := NewWHIPHandler(newTestSubsystem(t), 0)
|
||||
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(http.MethodOptions, "/whip/any", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
c.SetParamNames("id")
|
||||
c.SetParamValues("any")
|
||||
|
||||
if err := h.preflight(c); err != nil {
|
||||
t.Fatalf("preflight returned error: %v", err)
|
||||
}
|
||||
|
||||
expose := rec.Header().Get("Access-Control-Expose-Headers")
|
||||
if !strings.Contains(expose, "Link") {
|
||||
t.Errorf("Access-Control-Expose-Headers %q does not contain 'Link'", expose)
|
||||
}
|
||||
if !strings.Contains(expose, "Location") {
|
||||
t.Errorf("Access-Control-Expose-Headers %q does not contain 'Location'", expose)
|
||||
}
|
||||
if !strings.Contains(expose, "ETag") {
|
||||
t.Errorf("Access-Control-Expose-Headers %q does not contain 'ETag'", expose)
|
||||
}
|
||||
}
|
||||
|
||||
// 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).
|
||||
func TestWHIPHandler_Publish_CORSHeadersPresent(t *testing.T) {
|
||||
h := NewWHIPHandler(newTestSubsystem(t), 0)
|
||||
|
||||
e := echo.New()
|
||||
req := httptest.NewRequest(http.MethodPost, "/whip/ghost", strings.NewReader("v=0\r\n"))
|
||||
rec := httptest.NewRecorder()
|
||||
c := e.NewContext(req, rec)
|
||||
c.SetParamNames("id")
|
||||
c.SetParamValues("ghost")
|
||||
|
||||
_ = h.Publish(c)
|
||||
|
||||
if rec.Header().Get("Access-Control-Allow-Origin") != "*" {
|
||||
t.Error("expected Access-Control-Allow-Origin: *")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue