Files
OwnCord/Server/ws/livekit_client_test.go
T
Claude 0918f859a0 test: close measured test-coverage gaps across server, client and Rust
Audits what actually has tests, then closes the gaps it found. Full write-up
with before/after numbers in docs/audit-test-coverage-2026-07-25.md.

Measurement first: `go test ./... -coverprofile` (what CI runs) instruments
each package only for itself, so code exercised through another package's
tests reads as uncovered — `service` reported 36.7% against a real 85%. All
analysis here uses -coverpkg=./..., and both views now have Makefile targets.

Features that had zero coverage at every layer:
- user blocking (db + service + the /api/v1/blocks routes)
- auth lockout persistence — the DB round-trip that survives a restart
- plugin install/enable/disable/uninstall and the plugin KV namespace
- event replay bounds (GetMaxEventSeq, PruneEventsOlderThan)
- LiveKit participant_joined webhook (replayed-token guard), the room-service
  client, and proxyWebSocket/copyWS
- ws_proxy.rs and livekit_proxy.rs — pure helpers extracted, matching the
  existing tofu.rs pattern, so cert-pin and header-injection checks are testable

Gaps that were hidden rather than absent:
- Server/admin reported 0.3% coverage with 307 tests passing. TestSpawnDetached_*
  re-execs the test binary; the child inherited GOCOVERDIR and the parent's
  stdout, clobbering the profile and printing "[no tests to run]". Now 71.4%,
  and CI's uploaded artifact is correct.
- vitest.config.ts excluded 2.2k LOC unexplained, including two files that
  already had tests. Trimmed to three entries, each justified inline.
- api.HandleLiveKitHealthForTest re-implemented the handler it claimed to
  expose, so eight call sites tested a copy. Added a hook to the real one.

Two bugs found and pinned rather than silently patched: logctx.WithGroup nests
req_id under the group, and drag-reorder.ts takes one listener ref per channel
but releases one per sidebar, so the count never reaches zero.

Coverage: client 92.93% -> 94.87% statements (3371 -> 3572 tests) even after
un-excluding hidden files; Rust 47 -> 74 tests; Go zero-coverage functions
~70 -> 21, with plugin 61->77%, admin 67->86%, db 76->84%, service 85->91%.

Verified: go vet, all four build-tag variants, go test -race, -tags deadlock,
vitest --coverage, cargo test --lib, cargo clippy --all-targets, playwright.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEETs3Vh6sAHHb1jMBL75g
2026-07-25 14:47:21 +00:00

221 lines
5.8 KiB
Go

package ws_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/livekit/protocol/livekit"
"google.golang.org/protobuf/proto"
"github.com/owncord/server/config"
"github.com/owncord/server/ws"
)
// LiveKitClient.ListParticipants, CountVideoTracks and HealthCheck had no
// coverage — CountVideoTracks in particular gates MaxVideo enforcement, so a
// miscount silently changes who is allowed to turn a camera on.
//
// The room service client speaks Twirp over HTTP, so these tests stand up an
// httptest server that replies with real protobuf-encoded responses.
// twirpServer returns an httptest server that answers every Twirp RPC with the
// supplied protobuf message, and a client pointed at it.
func twirpServer(t *testing.T, status int, reply proto.Message) *ws.LiveKitClient {
t.Helper()
var body []byte
if reply != nil {
var err error
body, err = proto.Marshal(reply)
if err != nil {
t.Fatalf("marshal reply: %v", err)
}
}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if status != http.StatusOK {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_, _ = w.Write([]byte(`{"code":"internal","msg":"boom"}`))
return
}
w.Header().Set("Content-Type", "application/protobuf")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(body)
}))
t.Cleanup(srv.Close)
client, err := ws.NewLiveKitClient(&config.VoiceConfig{
LiveKitAPIKey: "testkeytestkeytest",
LiveKitAPISecret: "testsecrettestsecrettestsecret",
LiveKitURL: "ws://" + srv.Listener.Addr().String(),
})
if err != nil {
t.Fatalf("NewLiveKitClient: %v", err)
}
return client
}
func TestLiveKitClient_ListParticipants_Empty(t *testing.T) {
client := twirpServer(t, http.StatusOK, &livekit.ListParticipantsResponse{})
got, err := client.ListParticipants(42)
if err != nil {
t.Fatalf("ListParticipants: %v", err)
}
if len(got) != 0 {
t.Errorf("ListParticipants = %v, want empty", got)
}
}
func TestLiveKitClient_ListParticipants_ReturnsParticipants(t *testing.T) {
client := twirpServer(t, http.StatusOK, &livekit.ListParticipantsResponse{
Participants: []*livekit.ParticipantInfo{
{Identity: "user-1:tok"},
{Identity: "user-2:tok"},
},
})
got, err := client.ListParticipants(42)
if err != nil {
t.Fatalf("ListParticipants: %v", err)
}
if len(got) != 2 {
t.Fatalf("ListParticipants returned %d participants, want 2", len(got))
}
if got[0].Identity != "user-1:tok" {
t.Errorf("participant[0].Identity = %q, want %q", got[0].Identity, "user-1:tok")
}
}
func TestLiveKitClient_ListParticipants_ServerError(t *testing.T) {
client := twirpServer(t, http.StatusInternalServerError, nil)
if _, err := client.ListParticipants(42); err == nil {
t.Error("ListParticipants against a failing server returned nil error")
}
}
func TestLiveKitClient_CountVideoTracks(t *testing.T) {
tests := []struct {
name string
participants []*livekit.ParticipantInfo
want int
}{
{
name: "no participants",
want: 0,
},
{
name: "audio only",
participants: []*livekit.ParticipantInfo{
{Tracks: []*livekit.TrackInfo{{Type: livekit.TrackType_AUDIO}}},
},
want: 0,
},
{
name: "one video among audio",
participants: []*livekit.ParticipantInfo{
{Tracks: []*livekit.TrackInfo{
{Type: livekit.TrackType_AUDIO},
{Type: livekit.TrackType_VIDEO},
}},
},
want: 1,
},
{
name: "video counted across participants",
participants: []*livekit.ParticipantInfo{
{Tracks: []*livekit.TrackInfo{{Type: livekit.TrackType_VIDEO}}},
{Tracks: []*livekit.TrackInfo{
{Type: livekit.TrackType_VIDEO},
{Type: livekit.TrackType_VIDEO},
}},
{Tracks: []*livekit.TrackInfo{{Type: livekit.TrackType_AUDIO}}},
},
want: 3,
},
{
name: "participant with no tracks",
participants: []*livekit.ParticipantInfo{
{Identity: "user-1"},
{Tracks: []*livekit.TrackInfo{{Type: livekit.TrackType_VIDEO}}},
},
want: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := twirpServer(t, http.StatusOK, &livekit.ListParticipantsResponse{
Participants: tt.participants,
})
got, err := client.CountVideoTracks(7)
if err != nil {
t.Fatalf("CountVideoTracks: %v", err)
}
if got != tt.want {
t.Errorf("CountVideoTracks = %d, want %d", got, tt.want)
}
})
}
}
func TestLiveKitClient_CountVideoTracks_PropagatesError(t *testing.T) {
client := twirpServer(t, http.StatusInternalServerError, nil)
got, err := client.CountVideoTracks(7)
if err == nil {
t.Fatal("CountVideoTracks against a failing server returned nil error")
}
if got != 0 {
t.Errorf("count = %d on error, want 0", got)
}
}
func TestLiveKitClient_HealthCheck_Success(t *testing.T) {
client := twirpServer(t, http.StatusOK, &livekit.ListRoomsResponse{})
ok, err := client.HealthCheck(context.Background())
if err != nil {
t.Fatalf("HealthCheck: %v", err)
}
if !ok {
t.Error("HealthCheck = false against a healthy server")
}
}
func TestLiveKitClient_HealthCheck_ServerError(t *testing.T) {
client := twirpServer(t, http.StatusInternalServerError, nil)
ok, err := client.HealthCheck(context.Background())
if err == nil {
t.Fatal("HealthCheck against a failing server returned nil error")
}
if ok {
t.Error("HealthCheck = true despite an error")
}
}
func TestLiveKitClient_HealthCheck_Unreachable(t *testing.T) {
client, err := ws.NewLiveKitClient(&config.VoiceConfig{
LiveKitAPIKey: "testkeytestkeytest",
LiveKitAPISecret: "testsecrettestsecrettestsecret",
LiveKitURL: "ws://127.0.0.1:1",
})
if err != nil {
t.Fatalf("NewLiveKitClient: %v", err)
}
ok, err := client.HealthCheck(context.Background())
if err == nil {
t.Fatal("HealthCheck against an unreachable server returned nil error")
}
if ok {
t.Error("HealthCheck = true against an unreachable server")
}
}