mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
Fixes all 109 golangci-lint findings (106 contextcheck, 1 gocritic,
2 gosec) that accumulated after D2 wired dbgen (whose queries take ctx)
under ctx-less db.DB wrappers while CI lint was quota-dead. No nolint
comments added; every finding fixed by genuinely threading context.
- db: all 138 hand-written db.DB methods take ctx first; the dbCtx()
Background shim is deleted; raw Query/QueryRow/Exec/Begin use their
Context variants; the four redundant ctx-less passthroughs removed.
db.Auditor/WriteAudit gain ctx.
- Seams: permissions.Checker (DB iface, HasChannelPerm,
RequireChannelAccess) and the service.Store interface mirror the new
signatures (ws.EventStore and plugin.PluginStore already did).
- Callers: api/admin handlers use r.Context(); ws per-message paths use
the connection ctx via DispatchV2; hub loops and startup wiring use
context.Background(); service methods thread ctx where they have one
and Background where no ctx exists. Public service surface reached by
ctx-holding chains (PermissionService.HasChannelPerm/GetRoleForUser/
RequireChannelAccess, message/dm/block/invite/profile methods) is now
ctx-first.
- Detached (context.WithoutCancel) where cancellation would break an
invariant, found by a 3-lens adversarial review of the diff:
* voice-leave background retries (a dead webhook/connection ctx killed
retry 2 before it ran, leaving ghost capacity-holding voice rows)
* rollbackVoiceJoin's compensating delete (its trigger IS the cancel)
* post-2FA-change DeleteOtherSessions and logout DeleteSession (the
security tail of a committed change must not die with the request)
* all api/ws audit writes (a banned user could suppress their own
login_blocked_banned row by aborting the request mid-bcrypt)
* admin backup VACUUM INTO (an interrupt left a truncated .db that
the backup list presented as restorable)
* post-commit message/edit refetches (a committed message must still
fan out when the sender disconnects)
* hub settings-cache refresh (one dead connection could pin stale
values for the 30s TTL)
- gocritic rangeValCopy fixed (index iteration); gosec G306 excluded in
config with justification (generated source must stay world-readable)
instead of flipping genprotocol output to 0o600.
Verified: gofmt/vet, all four build-tag variants, full suite, deadlock
pass, full -race pass, golangci-lint 0 issues uncapped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
432 lines
12 KiB
Go
432 lines
12 KiB
Go
package ws_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/owncord/server/db"
|
|
"github.com/owncord/server/ws"
|
|
)
|
|
|
|
// ─── IsUserConnected ────────────────────────────────────────────────────────
|
|
|
|
func TestIsUserConnected_NotConnected(t *testing.T) {
|
|
hub, _ := newCoverageHub(t)
|
|
|
|
if hub.IsUserConnected(9999) {
|
|
t.Error("expected false for unregistered user")
|
|
}
|
|
}
|
|
|
|
func TestIsUserConnected_Connected(t *testing.T) {
|
|
hub, database := newCoverageHub(t)
|
|
user := seedCoverageOwner(t, database, "connected-user")
|
|
send := make(chan []byte, 16)
|
|
c := ws.NewTestClientWithUser(hub, user, 0, send)
|
|
hub.Register(c)
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
if !hub.IsUserConnected(user.ID) {
|
|
t.Error("expected true for registered user")
|
|
}
|
|
}
|
|
|
|
func TestIsUserConnected_AfterUnregister(t *testing.T) {
|
|
hub, database := newCoverageHub(t)
|
|
user := seedCoverageOwner(t, database, "unreg-user")
|
|
send := make(chan []byte, 16)
|
|
c := ws.NewTestClientWithUser(hub, user, 0, send)
|
|
hub.Register(c)
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
hub.Unregister(c)
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
if hub.IsUserConnected(user.ID) {
|
|
t.Error("expected false after unregister")
|
|
}
|
|
}
|
|
|
|
// ─── qualityBitrate ─────────────────────────────────────────────────────────
|
|
|
|
func TestQualityBitrate_KnownPresets(t *testing.T) {
|
|
tests := []struct {
|
|
quality string
|
|
want int
|
|
}{
|
|
{"low", 32000},
|
|
{"medium", 64000},
|
|
{"high", 128000},
|
|
}
|
|
for _, tc := range tests {
|
|
got := ws.QualityBitrateForTest(tc.quality)
|
|
if got != tc.want {
|
|
t.Errorf("qualityBitrate(%q) = %d, want %d", tc.quality, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestQualityBitrate_UnknownFallsBackToMedium(t *testing.T) {
|
|
got := ws.QualityBitrateForTest("ultra")
|
|
if got != 64000 {
|
|
t.Errorf("qualityBitrate('ultra') = %d, want 64000 (medium fallback)", got)
|
|
}
|
|
}
|
|
|
|
func TestQualityBitrate_EmptyFallsBackToMedium(t *testing.T) {
|
|
got := ws.QualityBitrateForTest("")
|
|
if got != 64000 {
|
|
t.Errorf("qualityBitrate('') = %d, want 64000 (medium fallback)", got)
|
|
}
|
|
}
|
|
|
|
// ─── buildDMChannelOpen ─────────────────────────────────────────────────────
|
|
|
|
func TestBuildDMChannelOpen_NilRecipient(t *testing.T) {
|
|
result := ws.BuildDMChannelOpenForTest(1, nil)
|
|
if result != nil {
|
|
t.Error("expected nil for nil recipient")
|
|
}
|
|
}
|
|
|
|
func TestBuildDMChannelOpen_ValidRecipient(t *testing.T) {
|
|
avatar := "avatar.png"
|
|
user := &db.User{
|
|
ID: 42,
|
|
Username: "testuser",
|
|
Avatar: &avatar,
|
|
Status: "online",
|
|
}
|
|
|
|
result := ws.BuildDMChannelOpenForTest(100, user)
|
|
if result == nil {
|
|
t.Fatal("expected non-nil result for valid recipient")
|
|
}
|
|
if !json.Valid(result) {
|
|
t.Fatalf("result is not valid JSON: %s", result)
|
|
}
|
|
|
|
var msg struct {
|
|
Type string `json:"type"`
|
|
Payload struct {
|
|
ChannelID int64 `json:"channel_id"`
|
|
Recipient struct {
|
|
ID int64 `json:"id"`
|
|
Username string `json:"username"`
|
|
Avatar string `json:"avatar"`
|
|
Status string `json:"status"`
|
|
} `json:"recipient"`
|
|
} `json:"payload"`
|
|
}
|
|
if err := json.Unmarshal(result, &msg); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if msg.Payload.ChannelID != 100 {
|
|
t.Errorf("ChannelID = %d, want 100", msg.Payload.ChannelID)
|
|
}
|
|
if msg.Payload.Recipient.ID != 42 {
|
|
t.Errorf("Recipient.ID = %d, want 42", msg.Payload.Recipient.ID)
|
|
}
|
|
if msg.Payload.Recipient.Username != "testuser" {
|
|
t.Errorf("Username = %q, want 'testuser'", msg.Payload.Recipient.Username)
|
|
}
|
|
if msg.Payload.Recipient.Avatar != "avatar.png" {
|
|
t.Errorf("Avatar = %q, want 'avatar.png'", msg.Payload.Recipient.Avatar)
|
|
}
|
|
}
|
|
|
|
func TestBuildDMChannelOpen_NilAvatar(t *testing.T) {
|
|
user := &db.User{
|
|
ID: 43,
|
|
Username: "noavatar",
|
|
Avatar: nil,
|
|
Status: "offline",
|
|
}
|
|
|
|
result := ws.BuildDMChannelOpenForTest(200, user)
|
|
if result == nil {
|
|
t.Fatal("expected non-nil result")
|
|
}
|
|
|
|
var msg struct {
|
|
Payload struct {
|
|
Recipient struct {
|
|
Avatar string `json:"avatar"`
|
|
} `json:"recipient"`
|
|
} `json:"payload"`
|
|
}
|
|
_ = json.Unmarshal(result, &msg)
|
|
if msg.Payload.Recipient.Avatar != "" {
|
|
t.Errorf("Avatar = %q, want empty string for nil avatar", msg.Payload.Recipient.Avatar)
|
|
}
|
|
}
|
|
|
|
// ─── handleVoiceMute via HandleMessageForTest ───────────────────────────────
|
|
|
|
func TestHandleVoiceMute_NotInVoice2(t *testing.T) {
|
|
hub, database := newCoverageHub(t)
|
|
user := seedCoverageOwner(t, database, "mute-not-in-voice")
|
|
send := make(chan []byte, 16)
|
|
c := ws.NewTestClientWithUser(hub, user, 0, send)
|
|
hub.RegisterNowForTest(c)
|
|
|
|
payload := `{"muted":true}`
|
|
raw, _ := json.Marshal(map[string]any{"type": "voice_mute", "payload": json.RawMessage(payload)})
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
// Should receive an error about not being in a voice channel.
|
|
time.Sleep(10 * time.Millisecond)
|
|
found := false
|
|
for len(send) > 0 {
|
|
msg := <-send
|
|
var m struct {
|
|
Type string `json:"type"`
|
|
Payload struct {
|
|
Message string `json:"message"`
|
|
} `json:"payload"`
|
|
}
|
|
_ = json.Unmarshal(msg, &m)
|
|
if m.Type == "error" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("expected error message when not in voice channel")
|
|
}
|
|
}
|
|
|
|
// ─── handleVoiceDeafen not in voice ─────────────────────────────────────────
|
|
|
|
func TestHandleVoiceDeafen_NotInVoice2(t *testing.T) {
|
|
hub, database := newCoverageHub(t)
|
|
user := seedCoverageOwner(t, database, "deafen-not-voice")
|
|
send := make(chan []byte, 16)
|
|
c := ws.NewTestClientWithUser(hub, user, 0, send)
|
|
hub.RegisterNowForTest(c)
|
|
|
|
payload := `{"deafened":true}`
|
|
raw, _ := json.Marshal(map[string]any{"type": "voice_deafen", "payload": json.RawMessage(payload)})
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
found := false
|
|
for len(send) > 0 {
|
|
msg := <-send
|
|
var m struct {
|
|
Type string `json:"type"`
|
|
}
|
|
_ = json.Unmarshal(msg, &m)
|
|
if m.Type == "error" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("expected error message when not in voice channel")
|
|
}
|
|
}
|
|
|
|
// ─── handleVoiceCamera not in voice ─────────────────────────────────────────
|
|
|
|
func TestHandleVoiceCamera_NotInVoice2(t *testing.T) {
|
|
hub, database := newCoverageHub(t)
|
|
user := seedCoverageOwner(t, database, "cam-not-voice")
|
|
send := make(chan []byte, 16)
|
|
c := ws.NewTestClientWithUser(hub, user, 0, send)
|
|
hub.RegisterNowForTest(c)
|
|
|
|
payload := `{"enabled":true}`
|
|
raw, _ := json.Marshal(map[string]any{"type": "voice_camera", "payload": json.RawMessage(payload)})
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
found := false
|
|
for len(send) > 0 {
|
|
msg := <-send
|
|
var m struct {
|
|
Type string `json:"type"`
|
|
}
|
|
_ = json.Unmarshal(msg, &m)
|
|
if m.Type == "error" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("expected error message when not in voice channel")
|
|
}
|
|
}
|
|
|
|
// ─── handleVoiceScreenshare not in voice ────────────────────────────────────
|
|
|
|
func TestHandleVoiceScreenshare_NotInVoice2(t *testing.T) {
|
|
hub, database := newCoverageHub(t)
|
|
user := seedCoverageOwner(t, database, "share-not-voice")
|
|
send := make(chan []byte, 16)
|
|
c := ws.NewTestClientWithUser(hub, user, 0, send)
|
|
hub.RegisterNowForTest(c)
|
|
|
|
payload := `{"enabled":true}`
|
|
raw, _ := json.Marshal(map[string]any{"type": "voice_screenshare", "payload": json.RawMessage(payload)})
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
found := false
|
|
for len(send) > 0 {
|
|
msg := <-send
|
|
var m struct {
|
|
Type string `json:"type"`
|
|
}
|
|
_ = json.Unmarshal(msg, &m)
|
|
if m.Type == "error" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("expected error message when not in voice channel")
|
|
}
|
|
}
|
|
|
|
// ─── handleVoiceMute/Deafen bad payload ─────────────────────────────────────
|
|
|
|
func TestHandleVoiceMute_BadPayload(t *testing.T) {
|
|
hub, database := newCoverageHub(t)
|
|
user := seedCoverageOwner(t, database, "mute-bad-payload")
|
|
chanID, _ := database.CreateChannel(context.Background(), "mute-bp-ch", "voice", "", "", 0)
|
|
_ = database.JoinVoiceChannel(context.Background(), user.ID, chanID)
|
|
|
|
send := make(chan []byte, 16)
|
|
c := ws.NewTestClientWithUser(hub, user, 0, send)
|
|
ws.SetClientVoiceChID(c, chanID)
|
|
hub.RegisterNowForTest(c)
|
|
|
|
raw, _ := json.Marshal(map[string]any{"type": "voice_mute", "payload": json.RawMessage(`{invalid json`)})
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
found := false
|
|
for len(send) > 0 {
|
|
msg := <-send
|
|
var m struct {
|
|
Type string `json:"type"`
|
|
}
|
|
_ = json.Unmarshal(msg, &m)
|
|
if m.Type == "error" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("expected error for bad payload")
|
|
}
|
|
}
|
|
|
|
func TestHandleVoiceDeafen_BadPayload(t *testing.T) {
|
|
hub, database := newCoverageHub(t)
|
|
user := seedCoverageOwner(t, database, "deafen-bad-payload")
|
|
chanID, _ := database.CreateChannel(context.Background(), "deafen-bp-ch", "voice", "", "", 0)
|
|
_ = database.JoinVoiceChannel(context.Background(), user.ID, chanID)
|
|
|
|
send := make(chan []byte, 16)
|
|
c := ws.NewTestClientWithUser(hub, user, 0, send)
|
|
ws.SetClientVoiceChID(c, chanID)
|
|
hub.RegisterNowForTest(c)
|
|
|
|
raw, _ := json.Marshal(map[string]any{"type": "voice_deafen", "payload": json.RawMessage(`not json`)})
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
found := false
|
|
for len(send) > 0 {
|
|
msg := <-send
|
|
var m struct {
|
|
Type string `json:"type"`
|
|
}
|
|
_ = json.Unmarshal(msg, &m)
|
|
if m.Type == "error" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("expected error for bad payload")
|
|
}
|
|
}
|
|
|
|
// ─── handleVoiceCamera bad payload ──────────────────────────────────────────
|
|
|
|
func TestHandleVoiceCamera_BadPayload(t *testing.T) {
|
|
hub, database := newCoverageHub(t)
|
|
user := seedCoverageOwner(t, database, "cam-bad-payload")
|
|
chanID, _ := database.CreateChannel(context.Background(), "cam-bp-ch", "voice", "", "", 0)
|
|
_ = database.JoinVoiceChannel(context.Background(), user.ID, chanID)
|
|
|
|
send := make(chan []byte, 16)
|
|
c := ws.NewTestClientWithUser(hub, user, 0, send)
|
|
ws.SetClientVoiceChID(c, chanID)
|
|
ws.SetClientVoiceStateForTest(c, chanID, "join-token-fake")
|
|
hub.RegisterNowForTest(c)
|
|
|
|
raw, _ := json.Marshal(map[string]any{"type": "voice_camera", "payload": json.RawMessage(`{bad`)})
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
found := false
|
|
for len(send) > 0 {
|
|
msg := <-send
|
|
var m struct {
|
|
Type string `json:"type"`
|
|
}
|
|
_ = json.Unmarshal(msg, &m)
|
|
if m.Type == "error" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("expected error for bad payload")
|
|
}
|
|
}
|
|
|
|
// ─── handleVoiceScreenshare bad payload ─────────────────────────────────────
|
|
|
|
func TestHandleVoiceScreenshare_BadPayload(t *testing.T) {
|
|
hub, database := newCoverageHub(t)
|
|
user := seedCoverageOwner(t, database, "share-bad-payload")
|
|
chanID, _ := database.CreateChannel(context.Background(), "share-bp-ch", "voice", "", "", 0)
|
|
_ = database.JoinVoiceChannel(context.Background(), user.ID, chanID)
|
|
|
|
send := make(chan []byte, 16)
|
|
c := ws.NewTestClientWithUser(hub, user, 0, send)
|
|
ws.SetClientVoiceChID(c, chanID)
|
|
ws.SetClientVoiceStateForTest(c, chanID, "join-token-fake")
|
|
hub.RegisterNowForTest(c)
|
|
|
|
raw, _ := json.Marshal(map[string]any{"type": "voice_screenshare", "payload": json.RawMessage(`{bad`)})
|
|
hub.HandleMessageForTest(c, raw)
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
found := false
|
|
for len(send) > 0 {
|
|
msg := <-send
|
|
var m struct {
|
|
Type string `json:"type"`
|
|
}
|
|
_ = json.Unmarshal(msg, &m)
|
|
if m.Type == "error" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("expected error for bad payload")
|
|
}
|
|
}
|
|
|
|
// ─── leaveVoiceChannelWithRetry empty token ─────────────────────────────────
|
|
|
|
func TestLeaveVoiceChannelWithRetry_EmptyToken(t *testing.T) {
|
|
hub, _ := newCoverageHub(t)
|
|
|
|
err := ws.LeaveVoiceChannelWithRetryForTest(hub, 1, 1, "")
|
|
if err != nil {
|
|
t.Errorf("expected nil error for empty token, got %v", err)
|
|
}
|
|
}
|