Files
OwnCord/Server/ws/export_test.go
T
J3vbandClaude Fable 5 f1a673e87e fix: 35 findings from the 2026-08-22 bug hunt (#1402)
* fix(voice): 1 defect(s) (OC-0277)

* fix(voice): 1 defect(s) (OC-0278)

* fix(client): 1 defect(s) (OC-0280)

refreshDmSidebar() rebuilds the entire DM sidebar subtree on every
dmStore.channels change - which includes presence flips and new
messages, not just DM list changes. The "Find a conversation" filter
text and input focus live only in that destroyed subtree, so they were
silently wiped mid-typing. Capture and restore both across the
destroy+recreate cycle.

* fix(ws): 1 defect(s) (OC-0285)

* fix(client): 1 defect(s) (OC-0286)

* fix(client): 1 defect(s) (OC-0288)

Consume the legacy unscoped mute key after migrating it onto the first
host, so a brand-new host with no scoped key of its own no longer reads
through to the same legacy list and inherits another server's mutes.

* fix(voice): 1 defect(s) (OC-0290)

* fix(db): 1 defect(s) (OC-0293)

DecrementMentionCounts reversed mention_count bumps that were never
applied: message_mentions stores every resolved mention id including the
author's blockers, while applyMentionCounts excludes blockers before
incrementing. Deleting a blocked author's message therefore wiped an
unrelated, genuine mention badge on the same read_states row. Mirror the
block exclusion in the decrement UPDATE.

* fix(db): 1 defect(s) (OC-0294)

DeleteAccount soft-deletes the departing user's messages but never reversed the read_states.mention_count bumps those messages made, leaving phantom mention badges. Reverse them inline in the existing transaction, mirroring DecrementMentionCounts' guards.

* fix(client): 1 defect(s) (OC-0295)

MemberList rebuilt every row on any non-presence-only membersStore change
and on every roles_update, but registered each row's click/contextmenu
listeners on the component-lifetime disposable.signal, which only aborts
at destroy(). Discarded rows therefore stayed reachable (and their
listeners live) for the component's whole lifetime. Route per-row
listeners through a per-render AbortController that is aborted and
replaced at the top of every render, and aborted again in destroy().

* fix(identity): 1 defect(s) (OC-0297)

UpdateProfile's post-commit re-read of the user row could fail for reasons
unrelated to context cancellation (SQLITE_BUSY, I/O error, pool exhaustion)
and was reported as ErrInternal even though UpdateUserProfile had already
committed. Callers that treat any UpdateProfile error as proof the write
never landed — handleUploadAvatar deletes the file it just stored — would
delete a file the committed avatar column now points at, permanently
breaking the avatar with no user_update broadcast.

Since UpdateUserProfile only writes username/avatar/display_name/about,
merge those four onto the pre-write snapshot to reconstruct the committed
row without needing the re-read to succeed, and log the read failure.

* fix(ws): 2 defect(s) (OC-0298, OC-0299)

- OC-0298: applyConnectStatus stamped c.user.Status even when the
  UpdateUserStatus write failed, so auth_ok and the presence broadcast
  claimed a status users.status disagreed with, and buildReady's
  ListMembers read never self-corrected for the session.
- OC-0299: refreshUserSnapshot silently fell back to roleName "member"
  when the new role lookup failed, pinning the session to a fabricated
  role on the wire. It now fails closed like the sibling lookups in
  upgradeAndAuth and handleFreshConnect.

* fix(client): 1 defect(s) (OC-0300)

* fix(client): 1 defect(s) (OC-0301)

* fix(ws): 1 defect(s) (OC-0302)

* fix(api): 1 defect(s) (OC-0305)

handleDiagnosticsConnectivity used clientIP(r), ignoring cfg.Server.TrustedProxies, so behind a configured trusted reverse proxy the endpoint reported the proxy hop instead of the real client address. Use clientIPWithProxies with the parsed trusted-proxy nets, matching RateLimitMiddleware on the same route.

* fix(client): 2 defect(s) (OC-0306, OC-0308)

* fix(client): 1 defect(s) (OC-0307)

QuickSwitcher registered a per-row click listener against the
overlay-lifetime AbortSignal, but renderResults() rebuilds every row on
each keystroke, arrow key, and store refresh. Discarded rows kept their
listeners alive until the overlay closed. Replaced with one delegated
click listener on the stable results container, keyed off the
data-channelid each row already carries.

* fix(client): 1 defect(s) (OC-0310)

* fix(server): 3 defect(s) (OC-0279, OC-0291, OC-0292)

Reap a soft-deleted message's attachment files, count lapsed temporary
bans as active users in the require_2fa enrollment gate, and only apply
the 2FA-enrollment precondition when require_2fa itself is being enabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdkJRbjCtrG76jEnrhKbYo

* test(api): sync apiTestSchema with the user_blocks migration

DeleteAccount's mention-count reversal joins user_blocks; the api
package's hand-rolled schema fixture predates migration 012.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdkJRbjCtrG76jEnrhKbYo

* fix(client): 3 defect(s) (OC-0281, OC-0282, OC-0296)

Decouple the E2EE identity-mismatch modal and right-click popovers from
the sidebar's per-render abort signal, and let global drag listeners
survive a mid-drag re-render.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdkJRbjCtrG76jEnrhKbYo

* fix(voice): 2 defect(s) (OC-0283, OC-0287)

Retire a departed peer's E2EE key unconditionally on leave, and surface
a failed microphone unmute instead of reporting an unmuted state the
room never saw.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdkJRbjCtrG76jEnrhKbYo

* fix(client): 3 defect(s) (OC-0289, OC-0303, OC-0309)

Guard the DM call button against redialing the channel already joined,
resolve the incoming-call banner's caller through the nickname-aware
display name, and keep the DM profile sidebar subscribed to live
member/status updates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdkJRbjCtrG76jEnrhKbYo

* style(client): prettier-format the dm-store test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdkJRbjCtrG76jEnrhKbYo

* fix(server): 1 defect(s) (OC-0284)

Make message soft-delete a compare-and-set so a repeated chat_delete
cannot reverse mention counts twice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdkJRbjCtrG76jEnrhKbYo

* fix(server): 2 defect(s) (OC-0276, OC-0304)

Re-sync a resumed connection's voice E2EE peer keys in registerNow
(announce frames are unsequenced and cannot be replayed), and apply the
live-connection presence rule to every DM payload DMService builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdkJRbjCtrG76jEnrhKbYo

* chore(ledger): record the 2026-08-21 hunt findings as fixed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdkJRbjCtrG76jEnrhKbYo

* chore(ledger): independent revert-proof pass for OC-0276..OC-0310

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdkJRbjCtrG76jEnrhKbYo

* refactor(service): extract DeleteMessage authorization into a helper

Keeps DeleteMessage under the cyclop complexity ceiling after the
OC-0284 guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdkJRbjCtrG76jEnrhKbYo

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-22 06:34:58 +02:00

476 lines
18 KiB
Go

// export_test.go exposes unexported functions and methods for use in external
// test packages (package ws_test). This file is compiled only during "go test".
package ws
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os/exec"
"time"
"github.com/livekit/protocol/livekit"
"github.com/owncord/server/db"
)
// ─── hub sweep helpers ─────────────────────────────────────────────────────
// SweepStaleClientsForTest exposes sweepStaleClients for external tests.
func (h *Hub) SweepStaleClientsForTest() {
h.sweepStaleClients()
}
// SweepStaleVoiceStatesForTest exposes sweepStaleVoiceStates for external tests.
func (h *Hub) SweepStaleVoiceStatesForTest() {
h.sweepStaleVoiceStates()
}
// SweepRevokedSessionsForTest exposes sweepRevokedSessions for external tests.
func (h *Hub) SweepRevokedSessionsForTest() {
h.sweepRevokedSessions()
}
// SetClientLastActivityForTest overwrites a client's lastActivity timestamp.
func SetClientLastActivityForTest(c *Client, t time.Time) {
c.mu.Lock()
defer c.mu.Unlock()
c.lastActivity = t
}
// ─── client getter/setter helpers ──────────────────────────────────────────
// GetLastActivityForTest exposes Client.getLastActivity for external tests.
func GetLastActivityForTest(c *Client) time.Time {
return c.getLastActivity()
}
// ClearVoiceChIDForTest exposes Client.clearVoiceChID for external tests.
func ClearVoiceChIDForTest(c *Client) int64 {
return c.clearVoiceChID()
}
// SetVoiceChIDForTest sets the voice channel ID atomically, clearing the join
// token when leaving (chID 0) — the same contract production keeps via
// setVoiceState. Test-only: production has no set-channel-without-token path.
func SetVoiceChIDForTest(c *Client, chID int64) {
c.voiceMu.Lock()
defer c.voiceMu.Unlock()
c.voiceChID = chID
if chID == 0 {
c.voiceJoinToken = ""
}
}
// SetClientVoiceChID is an alias kept for existing tests.
func SetClientVoiceChID(c *Client, channelID int64) {
SetVoiceChIDForTest(c, channelID)
}
// SubscribedToVoiceTopicForTest reports whether c itself (identity compare,
// not just its userID) holds the subscription to channelID's voice topic.
func (h *Hub) SubscribedToVoiceTopicForTest(c *Client, channelID int64) bool {
h.pubsub.mu.RLock()
defer h.pubsub.mu.RUnlock()
return h.pubsub.topics[VoiceTopic(channelID)][c.userID] == c
}
// SubscribeVoiceTopicForTest subscribes c to channelID's voice topic, as the
// production voice_join flow does.
func (h *Hub) SubscribeVoiceTopicForTest(c *Client, channelID int64) {
h.pubsub.Subscribe(c, VoiceTopic(channelID))
}
// SubscribedToChannelTopicForTest reports whether c itself (identity compare,
// not just its userID) holds the subscription to channelID's channel topic.
func (h *Hub) SubscribedToChannelTopicForTest(c *Client, channelID int64) bool {
h.pubsub.mu.RLock()
defer h.pubsub.mu.RUnlock()
return h.pubsub.topics[ChannelTopic(channelID)][c.userID] == c
}
// ApplySetChannelIDForTest exposes Hub.applySetChannelID for external tests —
// the SetChannelID applier that channel_focus's handleMessage result runs
// through (subscribe + live re-validate, OC-0024).
func (h *Hub) ApplySetChannelIDForTest(c *Client, newChID int64) {
h.applySetChannelID(c, newChID)
}
// SetClientVoiceStateForTest sets both the voice channel and join token,
// representing a settled, already-completed voice session — see
// voiceJoinCompleted on Client (OC-0270) — rather than a join still racing
// its own in-flight supersession guards. Callers that specifically need the
// latter (e.g. to exercise those guards, or registerNow's OC-0270 transfer
// gate) must not use this helper; use setVoiceState directly instead.
func SetClientVoiceStateForTest(c *Client, channelID int64, joinToken string) {
c.voiceMu.Lock()
defer c.voiceMu.Unlock()
c.voiceChID = channelID
c.voiceJoinToken = joinToken
c.voiceJoinCompleted = true
}
// SetClientE2EEPubKeyForTest sets the E2EE public key on a client (no signature).
func SetClientE2EEPubKeyForTest(c *Client, key string) {
c.setE2EEPubKey(key, "")
}
// GetClientE2EEPubKeyForTest returns the E2EE public key from a client.
func GetClientE2EEPubKeyForTest(c *Client) string {
key, _ := c.getE2EEPubKey()
return key
}
// NewTestClient creates a client with a caller-supplied send channel; conn is nil.
func NewTestClient(hub *Hub, userID int64, send chan []byte) *Client {
return &Client{
hub: hub,
ctx: context.Background(),
userID: userID,
send: send,
sendHigh: send, // unified for test observability
sendLow: send,
}
}
// NewTestClientWithChannel creates a test client subscribed to a specific channel.
func NewTestClientWithChannel(hub *Hub, userID, channelID int64, send chan []byte) *Client {
return &Client{
hub: hub,
ctx: context.Background(),
userID: userID,
channelID: channelID,
send: send,
sendHigh: send, // unified for test observability
sendLow: send,
}
}
// NewTestClientWithUser creates a test client with an authenticated user record
// set. Use this when tests need the client to pass permission checks.
func NewTestClientWithUser(hub *Hub, user *db.User, channelID int64, send chan []byte) *Client {
return &Client{
hub: hub,
ctx: context.Background(),
userID: user.ID,
user: user,
channelID: channelID,
send: send,
sendHigh: send, // unified for test observability
sendLow: send,
}
}
// NewTestClientWithTokenHash creates a test client that carries a session token
// hash. Use this when tests need to exercise the periodic session-expiry check.
func NewTestClientWithTokenHash(hub *Hub, user *db.User, tokenHash string, channelID int64, send chan []byte) *Client {
return &Client{
hub: hub,
ctx: context.Background(),
userID: user.ID,
user: user,
tokenHash: tokenHash,
channelID: channelID,
send: send,
sendHigh: send, // unified for test observability
sendLow: send,
}
}
// RunningForTest reports whether the hub's Run loop has started.
func (h *Hub) RunningForTest() bool {
return h.running.Load()
}
// ClientUserIDForTest returns the client's user ID for external tests.
func ClientUserIDForTest(c *Client) int64 {
return c.userID
}
// ClientChannelIDForTest returns the client's currently focused channel for
// external tests.
func ClientChannelIDForTest(c *Client) int64 {
return c.getChannelID()
}
// TouchForTest exposes Client.touch for external tests.
func TouchForTest(c *Client) {
c.touch()
}
// RollbackVoiceJoinForTest exposes Hub.rollbackVoiceJoin for external tests,
// exercising the empty-joinedAt (re-read) path.
func (h *Hub) RollbackVoiceJoinForTest(c *Client, channelID int64) {
h.rollbackVoiceJoin(context.Background(), c, channelID, "", true)
}
// RollbackVoiceJoinWithTokenForTest exposes Hub.rollbackVoiceJoin with an
// explicit join token, for external tests exercising the join-instance-scoped
// delete (OC-0044).
func (h *Hub) RollbackVoiceJoinWithTokenForTest(c *Client, channelID int64, joinedAt string) {
h.rollbackVoiceJoin(context.Background(), c, channelID, joinedAt, true)
}
// LeaveVoiceChannelWithRetryForTest exposes leaveVoiceChannelWithRetry for external tests.
func LeaveVoiceChannelWithRetryForTest(h *Hub, userID int64, channelID int64, joinToken string) error {
return leaveVoiceChannelWithRetry(context.Background(), h, userID, channelID, joinToken)
}
// ─── livekit process/webhook helpers ───────────────────────────────────────
// GenerateConfigForTest exposes LiveKitProcess.generateConfig for external tests.
func (p *LiveKitProcess) GenerateConfigForTest() (string, error) {
return p.generateConfig()
}
// HTTPTransportForTest exposes the health-check client's transport so tests can
// assert it does not share http.DefaultTransport's connection pool.
func (p *LiveKitProcess) HTTPTransportForTest() http.RoundTripper {
return p.httpClient.Transport
}
// SetProcessCmdForTest sets cmd to a non-nil value to simulate "already running".
func (p *LiveKitProcess) SetProcessCmdForTest() {
p.mu.Lock()
defer p.mu.Unlock()
p.cmd = &exec.Cmd{}
}
// SetProcessStoppedForTest sets stopped=true to simulate a stopped process.
func (p *LiveKitProcess) SetProcessStoppedForTest() {
p.mu.Lock()
defer p.mu.Unlock()
p.stopped = true
}
// NewHubForTest creates a minimal Hub with no DB or limiter for webhook testing.
func NewHubForTest() *Hub {
return &Hub{
clients: make(map[int64]*Client),
pubsub: NewPubSub(),
topicLimiter: NewTopicRateLimiter(topicRateLimitPerSecond, time.Second),
}
}
// PubSubForTest exposes the hub's PubSub for external tests.
func (h *Hub) PubSubForTest() *PubSub {
return h.pubsub
}
// BuildAuthOKForTest exposes Hub.buildAuthOK for external tests.
// Defaults to replay_source="none" since most callers test the fresh-connect
// path; tests that care about the resume tier can call buildAuthOK directly.
func (h *Hub) BuildAuthOKForTest(user *db.User, roleName string) []byte {
return h.buildAuthOK(context.Background(), user, roleName, "none")
}
// RunMentionCountsInlineForTest makes the hub's MessageService apply mention
// counts synchronously instead of on a background goroutine, so a test can read
// the counts deterministically right after driving a chat_send through the hub.
func (h *Hub) RunMentionCountsInlineForTest() {
h.messageSvc.RunBackgroundInlineForTest()
}
// BuildReadyForTest exposes Hub.buildReady for external tests.
// Passes nil role so no channels are visible (fail-closed, BUG-094).
func (h *Hub) BuildReadyForTest(database *db.DB, userID int64) ([]byte, error) {
return h.buildReady(context.Background(), database, userID, nil)
}
// BuildReadyWithRoleForTest exposes Hub.buildReady with a role for external tests.
func (h *Hub) BuildReadyWithRoleForTest(database *db.DB, userID int64, role *db.Role) ([]byte, error) {
return h.buildReady(context.Background(), database, userID, role)
}
// ComputeAllowedChannelsForTest exposes Hub.computeAllowedChannels for external
// tests (the REST/WS channel-visibility agreement test).
func (h *Hub) ComputeAllowedChannelsForTest(database *db.DB, user *db.User) (map[int64]bool, error) {
return h.computeAllowedChannels(context.Background(), database, user)
}
// GetCachedSettingsForTest exposes Hub.getCachedSettings for external tests.
func (h *Hub) GetCachedSettingsForTest() (string, string) {
return h.getCachedSettings(context.Background())
}
// GetClientVoiceChIDForTest exposes Client.getVoiceChID for external tests.
func GetClientVoiceChIDForTest(c *Client) int64 {
return c.getVoiceChID()
}
// GetClientVoiceJoinTokenForTest reads the join token under voiceMu.
func GetClientVoiceJoinTokenForTest(c *Client) string {
c.voiceMu.Lock()
defer c.voiceMu.Unlock()
return c.voiceJoinToken
}
// PeekClientPendingModFlagsForTest reads the moderator-stash flags
// (pendingModServerMuted/pendingModServerDeafened) without consuming them,
// unlike takePendingModFlags. Lets a test assert what a handler left behind
// without also clearing it out from under a later assertion.
func PeekClientPendingModFlagsForTest(c *Client) (serverMuted, serverDeafened bool) {
c.voiceMu.Lock()
defer c.voiceMu.Unlock()
return c.pendingModServerMuted, c.pendingModServerDeafened
}
// ExpireSettingsCacheForTest forces the settings cache to appear stale so that
// the next call to getCachedSettings triggers a DB refresh.
func (h *Hub) ExpireSettingsCacheForTest() {
h.settingsMu.Lock()
defer h.settingsMu.Unlock()
h.settingsLastUpdate = time.Time{} // zero time — always older than any TTL
}
// ParseChannelIDForTest exposes parseChannelID for external tests.
func ParseChannelIDForTest(payload json.RawMessage) (int64, error) {
return parseChannelID(payload)
}
// BuildJSONForTest exposes buildJSON for external tests.
func BuildJSONForTest(v any) []byte {
return buildJSON(v)
}
// ParseIdentityForTest parses a LiveKit participant identity and discards the
// join token, exercising the production parseParticipantIdentity.
func ParseIdentityForTest(identity string) (int64, error) {
userID, _, err := parseParticipantIdentity(identity)
return userID, err
}
// ParseParticipantIdentityForTest exposes parseParticipantIdentity for tests.
func ParseParticipantIdentityForTest(identity string) (int64, string, error) {
return parseParticipantIdentity(identity)
}
// ParseRoomChannelIDForTest exposes parseRoomChannelID for external tests.
func ParseRoomChannelIDForTest(roomName string) (int64, error) {
return parseRoomChannelID(roomName)
}
// WsToHTTPForTest exposes wsToHTTP for external tests.
func WsToHTTPForTest(wsURL string) string {
return wsToHTTP(wsURL)
}
// RegisterNowForTest exposes registerNow for external tests so clients are
// visible immediately (no channel round-trip through hub.Run). No channels are
// readable, matching the hub-loop registration path.
func (h *Hub) RegisterNowForTest(c *Client) {
h.registerNow(c, nil)
}
// RegisterNowWithReadableForTest exposes registerNow with an explicit
// READ_MESSAGES channel set, as the handshake paths in serve.go supply it.
func (h *Hub) RegisterNowWithReadableForTest(c *Client, readableChannelIDs map[int64]bool) {
h.registerNow(c, readableChannelIDs)
}
// ClearVoiceStateForTest exposes clearVoiceState for external tests.
func (c *Client) ClearVoiceStateForTest() {
c.clearVoiceState()
}
// QualityBitrateForTest exposes qualityBitrate for external tests.
func QualityBitrateForTest(quality string) int {
return qualityBitrate(quality)
}
// BuildDMChannelOpenForTest exposes buildDMChannelOpenFor for external tests.
func BuildDMChannelOpenForTest(channelID int64, recipient *db.User) []byte {
return buildDMChannelOpenFor(channelID, recipient, 0)
}
// BuildDMChannelOpenInfoForTest exposes the group-aware buildDMChannelOpen.
func BuildDMChannelOpenInfoForTest(info db.DMChannelInfo) []byte {
return buildDMChannelOpen(info)
}
// BuildCallSignalForTest exposes buildCallSignal for external tests.
func BuildCallSignalForTest(msgType string, channelID, fromUserID int64, username string) []byte {
return buildCallSignal(msgType, channelID, fromUserID, username)
}
// HandleWebhookParticipantLeftForTest exposes handleWebhookParticipantLeft for
// external tests so they can simulate LiveKit webhook events without HTTP.
func (h *Hub) HandleWebhookParticipantLeftForTest(userID int64, channelID int64, joinToken string) {
h.HandleWebhookParticipantLeftWithContextForTest(context.Background(), userID, channelID, joinToken)
}
// HandleWebhookParticipantLeftWithContextForTest is
// HandleWebhookParticipantLeftForTest with a caller-supplied context, so
// external tests can simulate the webhook HTTP handler's request context
// (e.g. already-cancelled, as it would be after the webhook sender hangs up)
// instead of always running with context.Background().
func (h *Hub) HandleWebhookParticipantLeftWithContextForTest(ctx context.Context, userID int64, channelID int64, joinToken string) {
identity := fmt.Sprintf("user-%d:%s", userID, joinToken)
roomName := fmt.Sprintf("channel-%d", channelID)
event := &livekit.WebhookEvent{
Event: "participant_left",
Participant: &livekit.ParticipantInfo{
Identity: identity,
},
Room: &livekit.Room{
Name: roomName,
},
}
h.handleWebhookParticipantLeft(ctx, event)
}
// HandleWebhookParticipantJoinedForTest exposes handleWebhookParticipantJoined
// for external tests. identity and roomName are passed raw so a test can feed
// malformed values through the same parse path a hostile webhook would.
func (h *Hub) HandleWebhookParticipantJoinedForTest(identity, roomName string) {
h.HandleWebhookParticipantJoinedWithContextForTest(context.Background(), identity, roomName)
}
// HandleWebhookParticipantJoinedWithContextForTest is
// HandleWebhookParticipantJoinedForTest with a caller-supplied context, so
// external tests can simulate the webhook HTTP handler's request context
// (e.g. already-cancelled, as it would be after the webhook sender hangs up)
// instead of always running with context.Background(). Mirrors
// HandleWebhookParticipantLeftWithContextForTest.
func (h *Hub) HandleWebhookParticipantJoinedWithContextForTest(ctx context.Context, identity, roomName string) {
event := &livekit.WebhookEvent{
Event: "participant_joined",
Participant: &livekit.ParticipantInfo{Identity: identity},
Room: &livekit.Room{Name: roomName},
}
h.handleWebhookParticipantJoined(ctx, event)
}
// WebhookMaxBodyBytesForTest exposes the webhook body cap so external tests can
// build a body that is over it without hardcoding the constant twice.
const WebhookMaxBodyBytesForTest = webhookMaxBodyBytes
// HandleWebhookParticipantJoinedEventForTest exposes
// handleWebhookParticipantJoined with a caller-built event so tests can cover
// the nil-participant and nil-room guards.
func (h *Hub) HandleWebhookParticipantJoinedEventForTest(event *livekit.WebhookEvent) {
h.handleWebhookParticipantJoined(context.Background(), event)
}
// MustFullResyncForTest exposes mustFullResync for external tests.
func (h *Hub) MustFullResyncForTest(lastSeq uint64) bool {
return h.mustFullResync(lastSeq)
}
// HasChannelPermForTest exposes Hub.hasChannelPerm for external tests.
func (h *Hub) HasChannelPermForTest(c *Client, channelID, perm int64) bool {
return h.hasChannelPerm(context.Background(), c, channelID, perm)
}
// BroadcastVoiceEventForTest exposes Hub.broadcastVoiceEvent for external
// tests so a load/soak test can drive the channelReadAudience-resolved
// voice_state/voice_leave fan-out directly, without a full LiveKit join
// round-trip.
func (h *Hub) BroadcastVoiceEventForTest(channelID int64, msg []byte) {
h.broadcastVoiceEvent(context.Background(), channelID, msg)
}
// MaxColdReplayForTest exposes the cold-tier replay row cap so tests can seed
// exactly enough events to hit it.
const MaxColdReplayForTest = maxColdReplay