test: port provider unit tests and fix API regressions from master

- Ported unit tests for WB Stream, SaluteJazz, and Telemost API clients
- Ported engine helper tests for SaluteJazz and Goolom
- Fixed missing WB Stream WebSocket URL update (wss://rtc-el-01.wb.ru)
- Corrected SaluteJazz header casing (X-Jazz-AuthType)
- Fixed linting errors (goconst, gosec, lll) in test files
This commit is contained in:
zarazaex69
2026-05-12 22:00:30 +03:00
parent 2d3a1152a1
commit 65555a47c8
8 changed files with 614 additions and 4 deletions

View File

@@ -17,12 +17,12 @@ import (
const (
authTypeAnonymous = "ANONYMOUS"
headerAuthType = "X-Jazz-Authtype"
headerAuthType = "X-Jazz-AuthType"
headerContentType = "Content-Type"
contentTypeJSON = "application/json"
)
var apiBase = "https://bk.salutejazz.ru" //nolint:gochecknoglobals // overridable base URL for tests
var apiBase = "https://bk.salutejazz.ru" //nolint:gochecknoglobals // package-level state intentional
// roomInfo contains connection details for a SaluteJazz room.
type roomInfo struct {

View File

@@ -0,0 +1,143 @@
package salutejazz
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/openlibrecommunity/olcrtc/internal/auth"
)
func withJazzAPIServer(t *testing.T, h http.Handler) {
t.Helper()
old := apiBase
srv := httptest.NewServer(h)
t.Cleanup(func() {
apiBase = old
srv.Close()
})
apiBase = srv.URL
}
func TestCreateMeetingAndPreconnect(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("POST /room/create-meeting", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Jazz-Authtype") != authTypeAnonymous {
t.Fatalf("missing auth header: %v", r.Header)
}
_ = json.NewEncoder(w).Encode(createResponse{RoomID: "room-1", Password: "pass"}) //nolint:gosec
})
mux.HandleFunc("POST /room/room-1/preconnect", func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]string{connectorURLKey: testConnector})
})
withJazzAPIServer(t, mux)
headers := map[string]string{
headerAuthType: authTypeAnonymous,
"Content-Type": "application/json",
}
created, err := createMeeting(context.Background(), headers)
if err != nil {
t.Fatalf("createMeeting() error = %v", err)
}
if created.RoomID != "room-1" || created.Password != "pass" {
t.Fatalf("createMeeting() = %+v", created)
}
connector, err := preconnect(context.Background(), "room-1", "pass", headers)
if err != nil {
t.Fatalf("preconnect() error = %v", err)
}
if connector != testConnector {
t.Fatalf("preconnect() = %q", connector)
}
}
const (
testRoomID = "new-room"
testPassword = "new-pass"
testConnector = "wss://connector"
connectorURLKey = "connectorUrl"
)
func TestCreateRoomAndJoinRoom(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("POST /room/create-meeting", func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(createResponse{RoomID: testRoomID, Password: testPassword}) //nolint:gosec
})
mux.HandleFunc("POST /room/{id}/preconnect", func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]string{connectorURLKey: testConnector})
})
withJazzAPIServer(t, mux)
room, err := createRoom(context.Background())
if err != nil {
t.Fatalf("createRoom() error = %v", err)
}
if room.RoomID != testRoomID || room.Password != testPassword ||
room.ConnectorURL != testConnector {
t.Fatalf("createRoom() = %+v", room)
}
room, err = joinRoom(context.Background(), "existing", "secret")
if err != nil {
t.Fatalf("joinRoom() error = %v", err)
}
if room.RoomID != "existing" || room.Password != "secret" || room.ConnectorURL != testConnector {
t.Fatalf("joinRoom() = %+v", room)
}
}
func TestJazzAPIErrors(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/room/create-meeting", func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "bad", http.StatusTeapot)
})
mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "bad", http.StatusInternalServerError)
})
withJazzAPIServer(t, mux)
if _, err := createMeeting(context.Background(), nil); !errors.Is(err, errCreateRoomFailed) {
t.Fatalf("createMeeting() error = %v, want %v", err, errCreateRoomFailed)
}
if _, err := preconnect(context.Background(), "room", "pass", nil); !errors.Is(err, errPreconnectFailed) {
t.Fatalf("preconnect() error = %v, want %v", err, errPreconnectFailed)
}
}
func TestJazzIssue(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("POST /room/create-meeting", func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(createResponse{RoomID: testRoomID, Password: testPassword}) //nolint:gosec
})
mux.HandleFunc("POST /room/{id}/preconnect", func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]string{connectorURLKey: testConnector})
})
withJazzAPIServer(t, mux)
p := Provider{}
creds, err := p.Issue(context.Background(), auth.Config{
RoomURL: "any",
Name: "peer",
})
if err != nil {
t.Fatalf("Issue() error = %v", err)
}
if creds.URL != testConnector {
t.Fatalf("creds.URL = %q", creds.URL)
}
if creds.Token != testRoomID {
t.Fatalf("creds.Token = %q", creds.Token)
}
if creds.Extra["password"] != testPassword {
t.Fatalf("creds.Extra[password] = %q", creds.Extra["password"])
}
}

View File

@@ -0,0 +1,65 @@
package telemost
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func withTelemostAPIServer(t *testing.T, h http.Handler) {
t.Helper()
old := apiBase
srv := httptest.NewServer(h)
t.Cleanup(func() {
apiBase = old
srv.Close()
})
apiBase = srv.URL
}
func TestGetConnectionInfo(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("GET /conferences/{id...}", func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, "/conferences/room/id/connection") {
t.Fatalf("path = %q", r.URL.Path)
}
if r.URL.Query().Get("display_name") != "peer" {
t.Fatalf("display_name query = %q", r.URL.Query().Get("display_name"))
}
_ = json.NewEncoder(w).Encode(ConnectionInfo{
RoomID: "room",
PeerID: "peer-id",
Credentials: "creds",
})
})
withTelemostAPIServer(t, mux)
info, err := GetConnectionInfo(context.Background(), "room/id", "peer")
if err != nil {
t.Fatalf("GetConnectionInfo() error = %v", err)
}
if info.RoomID != "room" || info.PeerID != "peer-id" || info.Credentials != "creds" {
t.Fatalf("GetConnectionInfo() = %+v", info)
}
}
func TestGetConnectionInfoErrors(t *testing.T) {
withTelemostAPIServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "bad", http.StatusForbidden)
}))
if _, err := GetConnectionInfo(context.Background(), "room", "peer"); !errors.Is(err, ErrAPI) {
t.Fatalf("GetConnectionInfo() error = %v, want %v", err, ErrAPI)
}
withTelemostAPIServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("{"))
}))
if _, err := GetConnectionInfo(context.Background(), "room", "peer"); err == nil {
t.Fatal("GetConnectionInfo() unexpectedly accepted bad json")
}
}

View File

@@ -16,9 +16,9 @@ import (
"github.com/openlibrecommunity/olcrtc/internal/protect"
)
const wsURL = "wss://wbstream01-el.wb.ru:7880"
const wsURL = "wss://rtc-el-01.wb.ru"
var apiBase = "https://stream.wb.ru" //nolint:gochecknoglobals // overridable base URL for tests
var apiBase = "https://stream.wb.ru" //nolint:gochecknoglobals // package-level state intentional
var (
errGuestRegister = errors.New("guest register failed")

View File

@@ -0,0 +1,135 @@
package wbstream
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/openlibrecommunity/olcrtc/internal/auth"
)
const (
testAccessToken = "access"
testRoomID = "room"
testToken = "token"
testPeerName = "peer"
)
func withWBAPIServer(t *testing.T, h http.Handler) {
t.Helper()
old := apiBase
srv := httptest.NewServer(h)
t.Cleanup(func() {
apiBase = old
srv.Close()
})
apiBase = srv.URL
}
func TestWBStreamAPIHappyPath(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("POST /auth/api/v1/auth/user/guest-register", func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(guestRegisterResponse{AccessToken: testAccessToken}) //nolint:gosec
})
mux.HandleFunc("POST /api-room/api/v2/room", func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer "+testAccessToken {
t.Fatalf("room auth = %q", r.Header.Get("Authorization"))
}
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(createRoomResponse{RoomID: testRoomID})
})
mux.HandleFunc("POST /api-room/api/v1/room/"+testRoomID+"/join", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
mux.HandleFunc("GET /api-room-manager/v2/room/"+testRoomID+"/connection-details",
func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("displayName") != testPeerName {
t.Fatalf("displayName query = %q", r.URL.Query().Get("displayName"))
}
_ = json.NewEncoder(w).Encode(tokenResponse{RoomToken: testToken})
})
withWBAPIServer(t, mux)
access, err := registerGuest(context.Background(), testPeerName)
if err != nil {
t.Fatalf("registerGuest() error = %v", err)
}
if access != testAccessToken {
t.Fatalf("registerGuest() = %q", access)
}
room, err := createRoom(context.Background(), access)
if err != nil {
t.Fatalf("createRoom() error = %v", err)
}
if room != testRoomID {
t.Fatalf("createRoom() = %q", room)
}
if err := joinRoom(context.Background(), access, room); err != nil {
t.Fatalf("joinRoom() error = %v", err)
}
token, err := getToken(context.Background(), access, room, testPeerName)
if err != nil {
t.Fatalf("getToken() error = %v", err)
}
if token != testToken {
t.Fatalf("getToken() = %q", token)
}
}
func TestWBStreamAPIErrors(t *testing.T) {
withWBAPIServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "bad", http.StatusBadGateway)
}))
if _, err := registerGuest(context.Background(), testPeerName); !errors.Is(err, errGuestRegister) {
t.Fatalf("registerGuest() error = %v, want %v", err, errGuestRegister)
}
if _, err := createRoom(context.Background(), testAccessToken); !errors.Is(err, errCreateRoom) {
t.Fatalf("createRoom() error = %v, want %v", err, errCreateRoom)
}
if err := joinRoom(context.Background(), testAccessToken, testRoomID); !errors.Is(err, errJoinRoom) {
t.Fatalf("joinRoom() error = %v, want %v", err, errJoinRoom)
}
if _, err := getToken(context.Background(), testAccessToken, testRoomID, testPeerName); !errors.Is(err, errGetToken) {
t.Fatalf("getToken() error = %v, want %v", err, errGetToken)
}
}
func TestWBStreamIssue(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("POST /auth/api/v1/auth/user/guest-register", func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(guestRegisterResponse{AccessToken: testAccessToken}) //nolint:gosec
})
mux.HandleFunc("POST /api-room/api/v2/room", func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(createRoomResponse{RoomID: "created"})
})
mux.HandleFunc("POST /api-room/api/v1/room/{id}/join", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
mux.HandleFunc("GET /api-room-manager/v2/room/{id}/connection-details", func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(tokenResponse{RoomToken: testToken})
})
withWBAPIServer(t, mux)
p := Provider{}
creds, err := p.Issue(context.Background(), auth.Config{
RoomURL: "any",
Name: testPeerName,
})
if err != nil {
t.Fatalf("Issue() error = %v", err)
}
if creds.Token != testToken {
t.Fatalf("creds.Token = %q", creds.Token)
}
if creds.Extra["roomID"] != "created" {
t.Fatalf("creds.Extra[roomID] = %q", creds.Extra["roomID"])
}
}

View File

@@ -0,0 +1,85 @@
package goolom
import (
"testing"
"time"
)
//nolint:cyclop // table-driven test naturally has many branches
func TestSessionReconnectAndEndedHelpers(t *testing.T) {
s := &Session{
reconnectCh: make(chan struct{}, 2),
closeCh: make(chan struct{}),
keepAliveCh: make(chan struct{}),
sessionCloseCh: make(chan struct{}),
telemetryCh: make(chan struct{}, 1),
}
keepAliveCh, sessionCloseCh := s.resetSession()
if keepAliveCh == nil || sessionCloseCh == nil || keepAliveCh != s.keepAliveCh || sessionCloseCh != s.sessionCloseCh {
t.Fatal("resetSession() did not replace session channels")
}
s.subscriberReady.Store(true)
s.publisherReady.Store(true)
s.resetMediaState()
if s.subscriberReady.Load() || s.publisherReady.Load() || s.subscriberConn == nil || s.publisherConn == nil {
t.Fatal("resetMediaState() did not reset readiness")
}
s.queueReconnect()
select {
case <-s.reconnectCh:
default:
t.Fatal("queueReconnect() did not enqueue")
}
s.SetShouldReconnect(func() bool { return false })
s.queueReconnect()
select {
case <-s.reconnectCh:
t.Fatal("queueReconnect() enqueued despite policy=false")
default:
}
s.reconnectCh <- struct{}{}
s.reconnectCh <- struct{}{}
s.drainReconnectQueue()
select {
case <-s.reconnectCh:
t.Fatal("drainReconnectQueue() left queued item")
default:
}
s.telemetryActive.Store(true)
s.stopTelemetry()
select {
case <-s.telemetryCh:
default:
t.Fatal("stopTelemetry() did not signal active telemetry")
}
ended := ""
s.SetEndedCallback(func(reason string) { ended = reason })
s.signalEnded("done")
if !s.closed.Load() || ended != "done" {
t.Fatalf("signalEnded() closed=%v reason=%q", s.closed.Load(), ended)
}
}
func TestWaitForAckTimeoutAndClose(t *testing.T) {
s := &Session{
closeCh: make(chan struct{}),
ackWaiters: make(map[string]chan struct{}),
}
ch := s.registerAckWaiter("timeout")
if s.waitForAck("timeout", ch, time.Millisecond) {
t.Fatal("waitForAck(timeout) = true")
}
ch = s.registerAckWaiter("closed")
close(s.closeCh)
if s.waitForAck("closed", ch, time.Second) {
t.Fatal("waitForAck(closeCh) = true")
}
}

View File

@@ -0,0 +1,70 @@
package salutejazz
import (
"bytes"
"errors"
"io"
"testing"
)
func TestDataPacketRoundTrip(t *testing.T) {
payload := []byte("hello jazz")
raw := EncodeDataPacket(payload)
got, ok := DecodeDataPacket(raw)
if !ok {
t.Fatal("DecodeDataPacket() ok = false")
}
if !bytes.Equal(got, payload) {
t.Fatalf("DecodeDataPacket() = %q, want %q", got, payload)
}
}
func TestDecodeDataPacketRejectsMalformedPackets(t *testing.T) {
tests := [][]byte{
nil,
{0xff},
encodeField(1, 0, encodeVarint(0)),
{byte(2<<3 | 2), 10, 1},
{byte(3<<3 | 7), 0},
}
for _, raw := range tests {
if payload, ok := DecodeDataPacket(raw); ok {
t.Fatalf("DecodeDataPacket(%v) = (%q, true), want false", raw, payload)
}
}
}
func TestParseFieldsSkipsSupportedNonTargetWireTypes(t *testing.T) {
data := encodeField(1, 0, encodeVarint(150))
data = append(data, encodeField(3, 1, []byte("12345678"))...)
data = append(data, encodeField(4, 5, []byte("1234"))...)
data = append(data, encodeField(2, 2, []byte("target"))...)
got, ok := parseFields(data, 2)
if !ok || string(got) != "target" {
t.Fatalf("parseFields() = (%q, %v), want target", got, ok)
}
}
func TestByteReader(t *testing.T) {
r := &byteReader{data: []byte{1, 2, 3}}
b, err := r.ReadByte()
if err != nil || b != 1 {
t.Fatalf("ReadByte() = (%d, %v), want (1, nil)", b, err)
}
buf := make([]byte, 4)
n, err := r.Read(buf)
if err != nil || n != 2 || !bytes.Equal(buf[:n], []byte{2, 3}) {
t.Fatalf("Read() = (%d, %v, %v), want two bytes", n, err, buf[:n])
}
if _, err := r.ReadByte(); !errors.Is(err, io.EOF) {
t.Fatalf("ReadByte() error = %v, want EOF", err)
}
if n, err := r.Read(buf); !errors.Is(err, io.EOF) || n != 0 {
t.Fatalf("Read() = (%d, %v), want (0, EOF)", n, err)
}
}

View File

@@ -0,0 +1,112 @@
package salutejazz
import (
"context"
"errors"
"testing"
"github.com/pion/webrtc/v4"
)
//nolint:cyclop // table-driven test naturally has many branches
func TestSessionStateHelpers(t *testing.T) {
s := &Session{
reconnectCh: make(chan struct{}, 1),
closeCh: make(chan struct{}),
sessionCloseCh: make(chan struct{}),
sendQueue: make(chan []byte, 1),
subscriberConn: make(chan struct{}),
publisherConn: make(chan struct{}),
}
s.resetMediaState()
if s.subscriberReady.Load() || s.publisherReady.Load() || s.subscriberConn == nil || s.publisherConn == nil {
t.Fatal("resetMediaState() did not reset readiness")
}
if s.hasLocalVideoTracks() {
t.Fatal("hasLocalVideoTracks() = true without tracks")
}
if err := s.AddVideoTrack(nil); err != nil {
t.Fatalf("AddVideoTrack(nil) error = %v", err)
}
if !s.hasLocalVideoTracks() {
t.Fatal("hasLocalVideoTracks() = false after AddVideoTrack")
}
s.SetVideoTrackHandler(func(*webrtc.TrackRemote, *webrtc.RTPReceiver) {})
if s.videoTrackHandler() == nil {
t.Fatal("videoTrackHandler() = nil")
}
cfg := defaultWebRTCConfig()
if cfg.SDPSemantics != webrtc.SDPSemanticsUnifiedPlan || cfg.BundlePolicy != webrtc.BundlePolicyMaxBundle {
t.Fatalf("defaultWebRTCConfig() = %+v", cfg)
}
if s.buildAPI() == nil {
t.Fatal("buildAPI() returned nil")
}
}
func TestSessionCallbacksQueueReconnectAndClose(t *testing.T) {
s := &Session{
reconnectCh: make(chan struct{}, 1),
closeCh: make(chan struct{}),
sessionCloseCh: make(chan struct{}),
sendQueue: make(chan []byte, 1),
}
s.SetReconnectCallback(func(*webrtc.DataChannel) {})
s.SetShouldReconnect(func() bool { return true })
s.SetEndedCallback(func(string) {})
if s.onReconnect == nil || s.shouldReconnect == nil || s.onEnded == nil {
t.Fatal("callbacks were not stored")
}
s.queueReconnect()
select {
case <-s.reconnectCh:
default:
t.Fatal("queueReconnect() did not enqueue")
}
s.SetShouldReconnect(func() bool { return false })
s.queueReconnect()
select {
case <-s.reconnectCh:
t.Fatal("queueReconnect() enqueued despite policy=false")
default:
}
done := make(chan struct{})
go func() {
s.WatchConnection(context.Background())
close(done)
}()
if err := s.Close(); err != nil {
t.Fatalf("Close() error = %v", err)
}
<-done
if err := s.Send([]byte("closed")); !errors.Is(err, ErrDataChannelNotReady) {
t.Fatalf("Send() error = %v, want datachannel not ready", err)
}
}
func TestSessionCanSendVideoOnlyModes(t *testing.T) {
s := &Session{sendQueue: make(chan []byte, 1)}
s.subscriberReady.Store(true)
if !s.CanSend() {
t.Fatal("CanSend() = false for subscriber-ready session without local video")
}
_ = s.AddVideoTrack(nil)
if s.CanSend() {
t.Fatal("CanSend() = true with local video but publisher not ready")
}
s.publisherReady.Store(true)
if !s.CanSend() {
t.Fatal("CanSend() = false with subscriber and publisher ready")
}
s.closed.Store(true)
if s.CanSend() {
t.Fatal("CanSend() = true for closed session")
}
}