package webrtc import ( "net/http" "net/http/httptest" "strings" "testing" "github.com/labstack/echo/v4" "github.com/datarhei/core/v16/config" corewebrtc "github.com/datarhei/core/v16/core/webrtc" ) func newTestSubsystem(t *testing.T) *Subsystem { t.Helper() s, err := New(config.DataWebRTC{Enable: true}, nil) if err != nil { t.Fatalf("New: %v", err) } return s } // TestHandler_Subscribe_404WhenStreamMissing verifies the WHEP POST // returns 404 when no process has registered a stream for that id. func TestHandler_Subscribe_404WhenStreamMissing(t *testing.T) { h := NewHandler(newTestSubsystem(t), 0) e := echo.New() req := httptest.NewRequest(http.MethodPost, "/whep/ghost", strings.NewReader("v=0\r\n")) rec := httptest.NewRecorder() c := e.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues("ghost") if err := h.Subscribe(c); err != nil { t.Fatalf("Subscribe returned error: %v", err) } if rec.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d: %s", rec.Code, rec.Body.String()) } } // TestHandler_Subscribe_400OnEmptyBody verifies invalid SDP offers // short-circuit before any peer is created. Requires a registered // stream so lookup doesn't 404 first. func TestHandler_Subscribe_400OnEmptyBody(t *testing.T) { sub := newTestSubsystem(t) // Register a dummy stream so the handler reaches body validation. sub.mu.Lock() sub.streams["probe"] = &processStream{id: "probe"} // video/audio nil is fine here — we never get past body parse sub.mu.Unlock() h := NewHandler(sub, 0) e := echo.New() req := httptest.NewRequest(http.MethodPost, "/whep/probe", strings.NewReader("")) rec := httptest.NewRecorder() c := e.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues("probe") if err := h.Subscribe(c); err != nil { t.Fatalf("Subscribe returned error: %v", err) } if rec.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d: %s", rec.Code, rec.Body.String()) } } // TestHandler_Unsubscribe_204WhenUnknown verifies a DELETE with an // unknown resource id returns 204 (idempotent), per the WHEP spec // and the M2/M3 design's error matrix. Pre-M3 this returned 404; the // updated semantics let clients re-issue DELETE without erroring. func TestHandler_Unsubscribe_204WhenUnknown(t *testing.T) { h := NewHandler(newTestSubsystem(t), 0) e := echo.New() req := httptest.NewRequest(http.MethodDelete, "/whep/id/unknown", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) c.SetParamNames("id", "resource") c.SetParamValues("id", "unknown") if err := h.Unsubscribe(c); err != nil { t.Fatalf("Unsubscribe returned error: %v", err) } if rec.Code != http.StatusNoContent { t.Fatalf("expected 204, got %d", rec.Code) } } // TestSubsystem_ICEServerURIs_ReturnsConfiguredURIs verifies that // ICEServerURIs() surfaces the URIs from the core config — the same // values Pion uses when building its PeerConnection. A default-config // subsystem must return at least the two bundled STUN servers. func TestSubsystem_ICEServerURIs_ReturnsConfiguredURIs(t *testing.T) { sub := newTestSubsystem(t) uris := sub.ICEServerURIs() defaultURIs := corewebrtc.DefaultConfig().ICEServers if len(uris) != len(defaultURIs) { t.Fatalf("expected %d ICE server URIs, got %d", len(defaultURIs), len(uris)) } for i, want := range defaultURIs { if uris[i] != want { t.Errorf("ICEServerURIs[%d]: want %q, got %q", i, want, uris[i]) } } } // TestAddCORS_ExposesLinkHeader verifies that CORS preflight responses // include "Link" in Access-Control-Expose-Headers so browsers can read // the RFC 9429 §4.3 Link headers returned on the 201 Subscribe response. func TestAddCORS_ExposesLinkHeader(t *testing.T) { h := NewHandler(newTestSubsystem(t), 0) e := echo.New() req := httptest.NewRequest(http.MethodOptions, "/whep/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) } }