mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
test: boost server test coverage to 80%+ across all packages
Add comprehensive tests for db, storage, updater, and ws packages covering edge cases, error paths, and voice handler functions. New test files for attachment queries and WebSocket coverage boost.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package db_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -732,3 +733,100 @@ func TestBackupToSafe_CreatesDirectoryFile(t *testing.T) {
|
||||
t.Error("backup file was not created")
|
||||
}
|
||||
}
|
||||
|
||||
// ─── UserCount ──────────────────────────────────────────────────────────────
|
||||
|
||||
func TestUserCount_Empty(t *testing.T) {
|
||||
database := newAdminTestDB(t)
|
||||
|
||||
count, err := database.UserCount()
|
||||
if err != nil {
|
||||
t.Fatalf("UserCount() error: %v", err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Errorf("UserCount() = %d, want 0", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserCount_WithUsers(t *testing.T) {
|
||||
database := newAdminTestDB(t)
|
||||
|
||||
for i := range 3 {
|
||||
_, err := database.CreateUser(
|
||||
fmt.Sprintf("countuser%d", i),
|
||||
"hash",
|
||||
4,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateUser[%d] error: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
count, err := database.UserCount()
|
||||
if err != nil {
|
||||
t.Fatalf("UserCount() error: %v", err)
|
||||
}
|
||||
if count != 3 {
|
||||
t.Errorf("UserCount() = %d, want 3", count)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── BackupTo ───────────────────────────────────────────────────────────────
|
||||
|
||||
func TestBackupToSafe_DirectCall(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dbPath := filepath.Join(tmpDir, "backup_src.db")
|
||||
|
||||
database, err := db.Open(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("db.Open: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
|
||||
migrFS := fstest.MapFS{
|
||||
"001_schema.sql": {Data: adminTestSchema},
|
||||
}
|
||||
if err := db.MigrateFS(database, migrFS); err != nil {
|
||||
t.Fatalf("MigrateFS: %v", err)
|
||||
}
|
||||
|
||||
backupDir := filepath.Join(tmpDir, "backups")
|
||||
_ = os.MkdirAll(backupDir, 0o755)
|
||||
backupPath := filepath.Join(backupDir, "backup_direct.db")
|
||||
if err := database.BackupToSafe(backupPath, backupDir); err != nil {
|
||||
t.Fatalf("BackupToSafe() error: %v", err)
|
||||
}
|
||||
|
||||
info, err := os.Stat(backupPath)
|
||||
if err != nil {
|
||||
t.Fatalf("backup file does not exist: %v", err)
|
||||
}
|
||||
if info.Size() == 0 {
|
||||
t.Error("backup file is empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupToSafe_RejectsTraversal(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dbPath := filepath.Join(tmpDir, "src.db")
|
||||
|
||||
database, err := db.Open(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("db.Open: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
|
||||
migrFS := fstest.MapFS{
|
||||
"001_schema.sql": {Data: adminTestSchema},
|
||||
}
|
||||
_ = db.MigrateFS(database, migrFS)
|
||||
|
||||
safeRoot := filepath.Join(tmpDir, "safe")
|
||||
_ = os.MkdirAll(safeRoot, 0o755)
|
||||
unsafePath := filepath.Join(tmpDir, "outside", "evil.db")
|
||||
|
||||
err = database.BackupToSafe(unsafePath, safeRoot)
|
||||
if err == nil {
|
||||
t.Error("BackupToSafe should reject path outside safe root")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
package db_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ─── GetAttachmentByID ──────────────────────────────────────────────────────
|
||||
|
||||
func TestGetAttachmentByID_NotFound(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
|
||||
_, err := database.GetAttachmentByID("nonexistent-id")
|
||||
if err == nil {
|
||||
t.Error("GetAttachmentByID for nonexistent ID should return error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAttachmentByID_Found(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
|
||||
// Insert an attachment directly.
|
||||
_, err := database.Exec(
|
||||
`INSERT INTO attachments (id, filename, stored_as, mime_type, size)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
"att-001", "photo.png", "stored-photo.png", "image/png", 12345,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("inserting attachment: %v", err)
|
||||
}
|
||||
|
||||
att, err := database.GetAttachmentByID("att-001")
|
||||
if err != nil {
|
||||
t.Fatalf("GetAttachmentByID: %v", err)
|
||||
}
|
||||
if att.ID != "att-001" {
|
||||
t.Errorf("ID = %q, want 'att-001'", att.ID)
|
||||
}
|
||||
if att.Filename != "photo.png" {
|
||||
t.Errorf("Filename = %q, want 'photo.png'", att.Filename)
|
||||
}
|
||||
if att.MimeType != "image/png" {
|
||||
t.Errorf("MimeType = %q, want 'image/png'", att.MimeType)
|
||||
}
|
||||
if att.Size != 12345 {
|
||||
t.Errorf("Size = %d, want 12345", att.Size)
|
||||
}
|
||||
if att.MessageID != nil {
|
||||
t.Errorf("MessageID = %v, want nil (unlinked)", att.MessageID)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── LinkAttachmentsToMessage ────────────────────────────────────────────────
|
||||
|
||||
func TestLinkAttachmentsToMessage_Empty(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
|
||||
n, err := database.LinkAttachmentsToMessage(1, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("LinkAttachmentsToMessage(nil): %v", err)
|
||||
}
|
||||
if n != 0 {
|
||||
t.Errorf("expected 0 rows affected, got %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinkAttachmentsToMessage_LinksUnlinked(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
userID := seedUser(t, database, "linkuser")
|
||||
chID := seedChannel(t, database, "linkchan")
|
||||
msgID, _ := database.CreateMessage(chID, userID, "with attachment", nil)
|
||||
|
||||
// Insert two unlinked attachments.
|
||||
for _, id := range []string{"att-a", "att-b"} {
|
||||
_, err := database.Exec(
|
||||
`INSERT INTO attachments (id, filename, stored_as, mime_type, size)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
id, "file.txt", "stored.txt", "text/plain", 100,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("inserting attachment %s: %v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
n, err := database.LinkAttachmentsToMessage(msgID, []string{"att-a", "att-b"})
|
||||
if err != nil {
|
||||
t.Fatalf("LinkAttachmentsToMessage: %v", err)
|
||||
}
|
||||
if n != 2 {
|
||||
t.Errorf("expected 2 rows affected, got %d", n)
|
||||
}
|
||||
|
||||
// Verify linkage.
|
||||
att, _ := database.GetAttachmentByID("att-a")
|
||||
if att.MessageID == nil || *att.MessageID != msgID {
|
||||
t.Errorf("att-a MessageID = %v, want %d", att.MessageID, msgID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinkAttachmentsToMessage_SkipsAlreadyLinked(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
userID := seedUser(t, database, "linkuser2")
|
||||
chID := seedChannel(t, database, "linkchan2")
|
||||
msg1, _ := database.CreateMessage(chID, userID, "msg1", nil)
|
||||
msg2, _ := database.CreateMessage(chID, userID, "msg2", nil)
|
||||
|
||||
_, _ = database.Exec(
|
||||
`INSERT INTO attachments (id, filename, stored_as, mime_type, size, message_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
"att-linked", "file.txt", "stored.txt", "text/plain", 100, msg1,
|
||||
)
|
||||
|
||||
// Try to re-link to a different message — should skip (WHERE message_id IS NULL).
|
||||
n, err := database.LinkAttachmentsToMessage(msg2, []string{"att-linked"})
|
||||
if err != nil {
|
||||
t.Fatalf("LinkAttachmentsToMessage: %v", err)
|
||||
}
|
||||
if n != 0 {
|
||||
t.Errorf("expected 0 rows (already linked), got %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── GetAttachmentsByMessageIDs ──────────────────────────────────────────────
|
||||
|
||||
func TestGetAttachmentsByMessageIDs_Empty(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
|
||||
result, err := database.GetAttachmentsByMessageIDs(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAttachmentsByMessageIDs(nil): %v", err)
|
||||
}
|
||||
if len(result) != 0 {
|
||||
t.Errorf("expected empty map, got %d entries", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAttachmentsByMessageIDs_GroupsByMessage(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
userID := seedUser(t, database, "attuser")
|
||||
chID := seedChannel(t, database, "attchan")
|
||||
msg1, _ := database.CreateMessage(chID, userID, "msg1", nil)
|
||||
msg2, _ := database.CreateMessage(chID, userID, "msg2", nil)
|
||||
|
||||
// Two attachments on msg1, one on msg2.
|
||||
for _, row := range []struct {
|
||||
id string
|
||||
msgID int64
|
||||
}{
|
||||
{"att-1a", msg1},
|
||||
{"att-1b", msg1},
|
||||
{"att-2a", msg2},
|
||||
} {
|
||||
_, err := database.Exec(
|
||||
`INSERT INTO attachments (id, filename, stored_as, mime_type, size, message_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
row.id, "f.txt", "s.txt", "text/plain", 50, row.msgID,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("insert %s: %v", row.id, err)
|
||||
}
|
||||
}
|
||||
|
||||
result, err := database.GetAttachmentsByMessageIDs([]int64{msg1, msg2})
|
||||
if err != nil {
|
||||
t.Fatalf("GetAttachmentsByMessageIDs: %v", err)
|
||||
}
|
||||
if len(result[msg1]) != 2 {
|
||||
t.Errorf("msg1 attachments = %d, want 2", len(result[msg1]))
|
||||
}
|
||||
if len(result[msg2]) != 1 {
|
||||
t.Errorf("msg2 attachments = %d, want 1", len(result[msg2]))
|
||||
}
|
||||
// Verify URL format.
|
||||
for _, ai := range result[msg1] {
|
||||
if ai.URL == "" {
|
||||
t.Error("attachment URL should not be empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -597,3 +597,134 @@ func TestUseInviteAtomic_ConcurrentSameCode(t *testing.T) {
|
||||
t.Errorf("use_count = %d after concurrent race, want 1", inv.Uses)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── UnbanUser ──────────────────────────────────────────────────────────────
|
||||
|
||||
func TestUnbanUser_ClearsBan(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
id, _ := database.CreateUser("unban_target", "hash", 4)
|
||||
|
||||
if err := database.BanUser(id, "spam", nil); err != nil {
|
||||
t.Fatalf("BanUser: %v", err)
|
||||
}
|
||||
|
||||
user, _ := database.GetUserByID(id)
|
||||
if !user.Banned {
|
||||
t.Fatal("user should be banned before unban")
|
||||
}
|
||||
|
||||
if err := database.UnbanUser(id); err != nil {
|
||||
t.Fatalf("UnbanUser: %v", err)
|
||||
}
|
||||
|
||||
user, _ = database.GetUserByID(id)
|
||||
if user.Banned {
|
||||
t.Error("Banned = true after UnbanUser, want false")
|
||||
}
|
||||
if user.BanReason != nil {
|
||||
t.Errorf("BanReason = %v, want nil after UnbanUser", user.BanReason)
|
||||
}
|
||||
if user.BanExpires != nil {
|
||||
t.Errorf("BanExpires = %v, want nil after UnbanUser", user.BanExpires)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnbanUser_NonexistentUser(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
|
||||
// Unbanning nonexistent user should not error.
|
||||
if err := database.UnbanUser(99999); err != nil {
|
||||
t.Errorf("UnbanUser(nonexistent) error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── ResetAllUserStatuses ───────────────────────────────────────────────────
|
||||
|
||||
func TestResetAllUserStatuses(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
id1, _ := database.CreateUser("status_u1", "hash", 4)
|
||||
id2, _ := database.CreateUser("status_u2", "hash", 4)
|
||||
|
||||
_ = database.UpdateUserStatus(id1, "online")
|
||||
_ = database.UpdateUserStatus(id2, "dnd")
|
||||
|
||||
if err := database.ResetAllUserStatuses(); err != nil {
|
||||
t.Fatalf("ResetAllUserStatuses: %v", err)
|
||||
}
|
||||
|
||||
u1, _ := database.GetUserByID(id1)
|
||||
u2, _ := database.GetUserByID(id2)
|
||||
if u1.Status != "offline" {
|
||||
t.Errorf("user1 status = %q, want 'offline'", u1.Status)
|
||||
}
|
||||
if u2.Status != "offline" {
|
||||
t.Errorf("user2 status = %q, want 'offline'", u2.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetAllUserStatuses_AlreadyOffline(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
_, _ = database.CreateUser("offline_user", "hash", 4)
|
||||
|
||||
// Should not error when all users are already offline.
|
||||
if err := database.ResetAllUserStatuses(); err != nil {
|
||||
t.Errorf("ResetAllUserStatuses: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── ListMembers ────────────────────────────────────────────────────────────
|
||||
|
||||
func TestListMembers_Empty(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
|
||||
members, err := database.ListMembers()
|
||||
if err != nil {
|
||||
t.Fatalf("ListMembers: %v", err)
|
||||
}
|
||||
if len(members) != 0 {
|
||||
t.Errorf("ListMembers() = %d, want 0", len(members))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListMembers_ExcludesBanned(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
id1, _ := database.CreateUser("member_visible", "hash", 4)
|
||||
id2, _ := database.CreateUser("member_banned", "hash", 4)
|
||||
_ = database.BanUser(id2, "test ban", nil)
|
||||
_ = id1 // suppress unused
|
||||
|
||||
members, err := database.ListMembers()
|
||||
if err != nil {
|
||||
t.Fatalf("ListMembers: %v", err)
|
||||
}
|
||||
if len(members) != 1 {
|
||||
t.Fatalf("ListMembers() = %d, want 1 (banned excluded)", len(members))
|
||||
}
|
||||
if members[0].Username != "member_visible" {
|
||||
t.Errorf("Username = %q, want 'member_visible'", members[0].Username)
|
||||
}
|
||||
if members[0].Role == "" {
|
||||
t.Error("Role should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListMembers_SortedByUsername(t *testing.T) {
|
||||
database := newTestDB(t)
|
||||
_, _ = database.CreateUser("zeta_user", "hash", 4)
|
||||
_, _ = database.CreateUser("alpha_user", "hash", 4)
|
||||
_, _ = database.CreateUser("mid_user", "hash", 4)
|
||||
|
||||
members, err := database.ListMembers()
|
||||
if err != nil {
|
||||
t.Fatalf("ListMembers: %v", err)
|
||||
}
|
||||
if len(members) != 3 {
|
||||
t.Fatalf("ListMembers() = %d, want 3", len(members))
|
||||
}
|
||||
if members[0].Username != "alpha_user" {
|
||||
t.Errorf("first member = %q, want 'alpha_user' (sorted)", members[0].Username)
|
||||
}
|
||||
if members[2].Username != "zeta_user" {
|
||||
t.Errorf("last member = %q, want 'zeta_user' (sorted)", members[2].Username)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,3 +233,61 @@ func TestGetChannelPermissions_WithOverride(t *testing.T) {
|
||||
t.Errorf("deny = %d, want 0x200", deny)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── SetChannelSlowMode ─────────────────────────────────────────────────────
|
||||
|
||||
func TestSetChannelSlowMode(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
chID, _ := database.CreateChannel("slowch", "text", "", "", 0)
|
||||
|
||||
if err := database.SetChannelSlowMode(chID, 10); err != nil {
|
||||
t.Fatalf("SetChannelSlowMode: %v", err)
|
||||
}
|
||||
|
||||
ch, _ := database.GetChannel(chID)
|
||||
if ch.SlowMode != 10 {
|
||||
t.Errorf("SlowMode = %d, want 10", ch.SlowMode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetChannelSlowMode_Zero(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
chID, _ := database.CreateChannel("slowch2", "text", "", "", 0)
|
||||
|
||||
_ = database.SetChannelSlowMode(chID, 30)
|
||||
_ = database.SetChannelSlowMode(chID, 0)
|
||||
|
||||
ch, _ := database.GetChannel(chID)
|
||||
if ch.SlowMode != 0 {
|
||||
t.Errorf("SlowMode = %d, want 0 (disabled)", ch.SlowMode)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── SetChannelVoiceMaxUsers ────────────────────────────────────────────────
|
||||
|
||||
func TestSetChannelVoiceMaxUsers(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
chID, _ := database.CreateChannel("voicech", "voice", "", "", 0)
|
||||
|
||||
if err := database.SetChannelVoiceMaxUsers(chID, 25); err != nil {
|
||||
t.Fatalf("SetChannelVoiceMaxUsers: %v", err)
|
||||
}
|
||||
|
||||
ch, _ := database.GetChannel(chID)
|
||||
if ch.VoiceMaxUsers != 25 {
|
||||
t.Errorf("VoiceMaxUsers = %d, want 25", ch.VoiceMaxUsers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetChannelVoiceMaxUsers_Unlimited(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
chID, _ := database.CreateChannel("voicech2", "voice", "", "", 0)
|
||||
|
||||
_ = database.SetChannelVoiceMaxUsers(chID, 10)
|
||||
_ = database.SetChannelVoiceMaxUsers(chID, 0)
|
||||
|
||||
ch, _ := database.GetChannel(chID)
|
||||
if ch.VoiceMaxUsers != 0 {
|
||||
t.Errorf("VoiceMaxUsers = %d, want 0 (unlimited)", ch.VoiceMaxUsers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,3 +521,203 @@ func TestUpdateReadState_Upsert(t *testing.T) {
|
||||
t.Fatalf("UpdateReadState second call: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── GetMessagesForAPI ──────────────────────────────────────────────────────
|
||||
|
||||
func TestGetMessagesForAPI_Empty(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
chID := seedChannel(t, database, "apichan")
|
||||
userID := seedUser(t, database, "apiuser")
|
||||
|
||||
msgs, err := database.GetMessagesForAPI(chID, 0, 50, userID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetMessagesForAPI: %v", err)
|
||||
}
|
||||
if len(msgs) != 0 {
|
||||
t.Errorf("expected 0 messages, got %d", len(msgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMessagesForAPI_ReturnsUserObject(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
userID := seedUser(t, database, "apiuser2")
|
||||
chID := seedChannel(t, database, "apichan2")
|
||||
|
||||
_, _ = database.CreateMessage(chID, userID, "hello api", nil)
|
||||
|
||||
msgs, err := database.GetMessagesForAPI(chID, 0, 50, userID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetMessagesForAPI: %v", err)
|
||||
}
|
||||
if len(msgs) != 1 {
|
||||
t.Fatalf("expected 1 message, got %d", len(msgs))
|
||||
}
|
||||
if msgs[0].User.Username != "apiuser2" {
|
||||
t.Errorf("User.Username = %q, want 'apiuser2'", msgs[0].User.Username)
|
||||
}
|
||||
if msgs[0].User.ID != userID {
|
||||
t.Errorf("User.ID = %d, want %d", msgs[0].User.ID, userID)
|
||||
}
|
||||
if msgs[0].Content != "hello api" {
|
||||
t.Errorf("Content = %q, want 'hello api'", msgs[0].Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMessagesForAPI_BeforePagination(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
userID := seedUser(t, database, "apipage")
|
||||
chID := seedChannel(t, database, "apich")
|
||||
|
||||
var ids []int64
|
||||
for range 5 {
|
||||
id, _ := database.CreateMessage(chID, userID, "msg", nil)
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
msgs, err := database.GetMessagesForAPI(chID, ids[3], 50, userID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetMessagesForAPI with before: %v", err)
|
||||
}
|
||||
if len(msgs) != 3 {
|
||||
t.Errorf("expected 3 messages before id %d, got %d", ids[3], len(msgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMessagesForAPI_WithReactions(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
u1 := seedUser(t, database, "reactuser1")
|
||||
u2 := seedUser(t, database, "reactuser2")
|
||||
chID := seedChannel(t, database, "reactchan")
|
||||
|
||||
msgID, _ := database.CreateMessage(chID, u1, "react me", nil)
|
||||
_ = database.AddReaction(msgID, u1, "👍")
|
||||
_ = database.AddReaction(msgID, u2, "👍")
|
||||
|
||||
msgs, err := database.GetMessagesForAPI(chID, 0, 50, u1)
|
||||
if err != nil {
|
||||
t.Fatalf("GetMessagesForAPI: %v", err)
|
||||
}
|
||||
if len(msgs) != 1 {
|
||||
t.Fatalf("expected 1 message, got %d", len(msgs))
|
||||
}
|
||||
if len(msgs[0].Reactions) != 1 {
|
||||
t.Fatalf("expected 1 reaction type, got %d", len(msgs[0].Reactions))
|
||||
}
|
||||
if msgs[0].Reactions[0].Count != 2 {
|
||||
t.Errorf("reaction count = %d, want 2", msgs[0].Reactions[0].Count)
|
||||
}
|
||||
if !msgs[0].Reactions[0].Me {
|
||||
t.Error("Me should be true for requesting user who reacted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMessagesForAPI_ExcludesDeleted(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
userID := seedUser(t, database, "apidel")
|
||||
chID := seedChannel(t, database, "apidelchan")
|
||||
|
||||
id, _ := database.CreateMessage(chID, userID, "deleted msg", nil)
|
||||
_ = database.DeleteMessage(id, userID, false)
|
||||
_, _ = database.CreateMessage(chID, userID, "visible msg", nil)
|
||||
|
||||
msgs, err := database.GetMessagesForAPI(chID, 0, 50, userID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetMessagesForAPI: %v", err)
|
||||
}
|
||||
if len(msgs) != 1 {
|
||||
t.Errorf("expected 1 message (deleted excluded), got %d", len(msgs))
|
||||
}
|
||||
}
|
||||
|
||||
// ─── GetChannelUnreadCounts ─────────────────────────────────────────────────
|
||||
|
||||
func TestGetChannelUnreadCounts_NoMessages(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
userID := seedUser(t, database, "unreaduser")
|
||||
_ = seedChannel(t, database, "unreadchan")
|
||||
|
||||
counts, err := database.GetChannelUnreadCounts(userID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetChannelUnreadCounts: %v", err)
|
||||
}
|
||||
// Should return entries for text channels even with 0 messages.
|
||||
if counts == nil {
|
||||
t.Fatal("GetChannelUnreadCounts returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetChannelUnreadCounts_WithUnreadMessages(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
userID := seedUser(t, database, "unreaduser2")
|
||||
chID := seedChannel(t, database, "unreadchan2")
|
||||
|
||||
// Create 3 messages, mark first as read.
|
||||
msg1, _ := database.CreateMessage(chID, userID, "msg1", nil)
|
||||
_, _ = database.CreateMessage(chID, userID, "msg2", nil)
|
||||
_, _ = database.CreateMessage(chID, userID, "msg3", nil)
|
||||
|
||||
_ = database.UpdateReadState(userID, chID, msg1)
|
||||
|
||||
counts, err := database.GetChannelUnreadCounts(userID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetChannelUnreadCounts: %v", err)
|
||||
}
|
||||
cu, ok := counts[chID]
|
||||
if !ok {
|
||||
t.Fatalf("channel %d not in unread counts", chID)
|
||||
}
|
||||
if cu.UnreadCount != 2 {
|
||||
t.Errorf("UnreadCount = %d, want 2", cu.UnreadCount)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── GetLatestMessageID ─────────────────────────────────────────────────────
|
||||
|
||||
func TestGetLatestMessageID_Empty(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
chID := seedChannel(t, database, "latestchan")
|
||||
|
||||
id, err := database.GetLatestMessageID(chID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetLatestMessageID: %v", err)
|
||||
}
|
||||
if id != 0 {
|
||||
t.Errorf("expected 0 for empty channel, got %d", id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLatestMessageID_ReturnsHighest(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
userID := seedUser(t, database, "latestuser")
|
||||
chID := seedChannel(t, database, "latestchan2")
|
||||
|
||||
_, _ = database.CreateMessage(chID, userID, "first", nil)
|
||||
_, _ = database.CreateMessage(chID, userID, "second", nil)
|
||||
lastID, _ := database.CreateMessage(chID, userID, "third", nil)
|
||||
|
||||
id, err := database.GetLatestMessageID(chID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetLatestMessageID: %v", err)
|
||||
}
|
||||
if id != lastID {
|
||||
t.Errorf("GetLatestMessageID = %d, want %d", id, lastID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLatestMessageID_ExcludesDeleted(t *testing.T) {
|
||||
database := openMigratedMemory(t)
|
||||
userID := seedUser(t, database, "latestdel")
|
||||
chID := seedChannel(t, database, "latestdelchan")
|
||||
|
||||
id1, _ := database.CreateMessage(chID, userID, "keep", nil)
|
||||
id2, _ := database.CreateMessage(chID, userID, "delete me", nil)
|
||||
_ = database.DeleteMessage(id2, userID, false)
|
||||
|
||||
latestID, err := database.GetLatestMessageID(chID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetLatestMessageID: %v", err)
|
||||
}
|
||||
if latestID != id1 {
|
||||
t.Errorf("GetLatestMessageID = %d, want %d (deleted excluded)", latestID, id1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,3 +387,90 @@ func TestSave_EmptyFileAllowed(t *testing.T) {
|
||||
t.Errorf("Save(empty) = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── New edge cases ──────────────────────────────────────────────────────────
|
||||
|
||||
func TestNew_CreatesDirectory(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
newDir := filepath.Join(tmpDir, "nested", "storage")
|
||||
|
||||
s, err := storage.New(newDir, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
if s == nil {
|
||||
t.Fatal("New returned nil")
|
||||
}
|
||||
|
||||
// Directory should exist.
|
||||
info, statErr := os.Stat(newDir)
|
||||
if statErr != nil {
|
||||
t.Fatalf("directory not created: %v", statErr)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
t.Error("expected directory, got file")
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Save large file ────────────────────────────────────────────────────────
|
||||
|
||||
func TestSave_ExceedsMaxSize(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
// 1 MB max.
|
||||
s, err := storage.New(tmpDir, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
|
||||
// Create reader with >1MB of data.
|
||||
bigData := bytes.Repeat([]byte("x"), 1024*1024+100)
|
||||
err = s.Save("big-file", bytes.NewReader(bigData))
|
||||
if err == nil {
|
||||
t.Error("Save should reject file exceeding max size")
|
||||
}
|
||||
|
||||
// File should be removed.
|
||||
if _, statErr := os.Stat(filepath.Join(tmpDir, "big-file")); !os.IsNotExist(statErr) {
|
||||
t.Error("oversized file should be removed after rejection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSave_ReadError(t *testing.T) {
|
||||
s := newTestStorage(t)
|
||||
err := s.Save("read-err", &failReader{})
|
||||
if err == nil {
|
||||
t.Error("Save with failing reader should return error")
|
||||
}
|
||||
}
|
||||
|
||||
type failReader struct{}
|
||||
|
||||
func (f *failReader) Read([]byte) (int, error) {
|
||||
return 0, errors.New("simulated read error")
|
||||
}
|
||||
|
||||
// ─── resolvedPath edge case (via Save with dot prefix) ──────────────────────
|
||||
|
||||
func TestSave_HiddenFilename(t *testing.T) {
|
||||
s := newTestStorage(t)
|
||||
err := s.Save(".hidden", strings.NewReader("data"))
|
||||
if err == nil {
|
||||
t.Error("Save should reject hidden filenames starting with '.'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpen_NotFound(t *testing.T) {
|
||||
s := newTestStorage(t)
|
||||
_, err := s.Open("nonexistent-file")
|
||||
if err == nil {
|
||||
t.Error("Open should return error for nonexistent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelete_NotFound(t *testing.T) {
|
||||
s := newTestStorage(t)
|
||||
err := s.Delete("nonexistent-file")
|
||||
if err == nil {
|
||||
t.Error("Delete should return error for nonexistent file")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,3 +263,219 @@ func TestParseChecksumFile_FileNotFound(t *testing.T) {
|
||||
t.Error("expected error for missing file in checksum data, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// ─── SetBaseURL ──────────────────────────────────────────────────────────────
|
||||
|
||||
func TestSetBaseURL(t *testing.T) {
|
||||
u := NewUpdater("1.0.0", "", "J3vb", "OwnCord")
|
||||
u.SetBaseURL("https://custom.api.example.com")
|
||||
if u.apiBaseURL() != "https://custom.api.example.com" {
|
||||
t.Errorf("apiBaseURL = %q, want custom URL", u.apiBaseURL())
|
||||
}
|
||||
}
|
||||
|
||||
func TestApiBaseURL_DefaultWhenEmpty(t *testing.T) {
|
||||
u := NewUpdater("1.0.0", "", "J3vb", "OwnCord")
|
||||
got := u.apiBaseURL()
|
||||
if got != defaultBaseURL {
|
||||
t.Errorf("apiBaseURL = %q, want default %q", got, defaultBaseURL)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── fetchBody ───────────────────────────────────────────────────────────────
|
||||
|
||||
func TestFetchBody_Success(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("hello body"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
u := newTestUpdater(srv.URL, "1.0.0")
|
||||
body, err := u.fetchBody(context.Background(), srv.URL+"/test")
|
||||
if err != nil {
|
||||
t.Fatalf("fetchBody: %v", err)
|
||||
}
|
||||
if string(body) != "hello body" {
|
||||
t.Errorf("body = %q, want 'hello body'", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchBody_NonOKStatus(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
u := newTestUpdater(srv.URL, "1.0.0")
|
||||
_, err := u.fetchBody(context.Background(), srv.URL+"/test")
|
||||
if err == nil {
|
||||
t.Error("fetchBody should error on non-200 status")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchBody_WithGithubToken(t *testing.T) {
|
||||
var gotAuth string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotAuth = r.Header.Get("Authorization")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
u := NewUpdater("1.0.0", "my-token", "J3vb", "OwnCord")
|
||||
u.baseURL = srv.URL
|
||||
_, _ = u.fetchBody(context.Background(), srv.URL+"/test")
|
||||
if gotAuth != "token my-token" {
|
||||
t.Errorf("Authorization = %q, want 'token my-token'", gotAuth)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── downloadFile ────────────────────────────────────────────────────────────
|
||||
|
||||
func TestDownloadFile_Success(t *testing.T) {
|
||||
content := []byte("binary content here")
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(content)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
dest := filepath.Join(tmpDir, "downloaded.exe")
|
||||
|
||||
u := newTestUpdater(srv.URL, "1.0.0")
|
||||
if err := u.downloadFile(context.Background(), srv.URL+"/binary", dest); err != nil {
|
||||
t.Fatalf("downloadFile: %v", err)
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(dest)
|
||||
if err != nil {
|
||||
t.Fatalf("reading downloaded file: %v", err)
|
||||
}
|
||||
if string(got) != string(content) {
|
||||
t.Errorf("content = %q, want %q", got, content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadFile_NonOKStatus(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
dest := filepath.Join(tmpDir, "downloaded.exe")
|
||||
|
||||
u := newTestUpdater(srv.URL, "1.0.0")
|
||||
err := u.downloadFile(context.Background(), srv.URL+"/binary", dest)
|
||||
if err == nil {
|
||||
t.Error("downloadFile should error on non-200 status")
|
||||
}
|
||||
}
|
||||
|
||||
// ─── DownloadAndVerify ───────────────────────────────────────────────────────
|
||||
|
||||
func TestDownloadAndVerify_Success(t *testing.T) {
|
||||
content := []byte("real binary content for verification")
|
||||
hash := sha256.Sum256(content)
|
||||
checksumHex := hex.EncodeToString(hash[:])
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/download/chatserver.exe", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(content)
|
||||
})
|
||||
mux.HandleFunc("/download/checksums.sha256", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprintf(w, "%s chatserver.exe\n", checksumHex)
|
||||
})
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
dest := filepath.Join(tmpDir, "chatserver.exe")
|
||||
|
||||
u := NewUpdater("1.0.0", "", "J3vb", "OwnCord")
|
||||
u.baseURL = srv.URL
|
||||
|
||||
downloadURL := fmt.Sprintf("https://github.com/J3vb/OwnCord/releases/download/v1.0.0/chatserver.exe")
|
||||
checksumURL := fmt.Sprintf("https://github.com/J3vb/OwnCord/releases/download/v1.0.0/checksums.sha256")
|
||||
|
||||
// Override HTTP client to route GitHub URLs to our test server.
|
||||
u.httpClient = &http.Client{
|
||||
Transport: &rewriteTransport{srv.URL},
|
||||
}
|
||||
|
||||
err := u.DownloadAndVerify(context.Background(), downloadURL, checksumURL, dest)
|
||||
if err != nil {
|
||||
t.Fatalf("DownloadAndVerify: %v", err)
|
||||
}
|
||||
|
||||
// File should exist and be correct.
|
||||
got, _ := os.ReadFile(dest)
|
||||
if string(got) != string(content) {
|
||||
t.Errorf("downloaded content mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadAndVerify_InvalidDownloadURL(t *testing.T) {
|
||||
u := NewUpdater("1.0.0", "", "J3vb", "OwnCord")
|
||||
err := u.DownloadAndVerify(context.Background(), "https://evil.com/file", "https://evil.com/sum", "/tmp/out")
|
||||
if err == nil {
|
||||
t.Error("DownloadAndVerify should reject invalid download URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadAndVerify_InvalidChecksumURL(t *testing.T) {
|
||||
u := NewUpdater("1.0.0", "", "J3vb", "OwnCord")
|
||||
downloadURL := "https://github.com/J3vb/OwnCord/releases/download/v1.0.0/chatserver.exe"
|
||||
err := u.DownloadAndVerify(context.Background(), downloadURL, "https://evil.com/sum", "/tmp/out")
|
||||
if err == nil {
|
||||
t.Error("DownloadAndVerify should reject invalid checksum URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadAndVerify_ChecksumMismatch(t *testing.T) {
|
||||
content := []byte("binary content")
|
||||
wrongChecksum := "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/download/chatserver.exe", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(content)
|
||||
})
|
||||
mux.HandleFunc("/download/checksums.sha256", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = fmt.Fprintf(w, "%s chatserver.exe\n", wrongChecksum)
|
||||
})
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
dest := filepath.Join(tmpDir, "chatserver.exe")
|
||||
|
||||
u := NewUpdater("1.0.0", "", "J3vb", "OwnCord")
|
||||
u.httpClient = &http.Client{Transport: &rewriteTransport{srv.URL}}
|
||||
|
||||
downloadURL := "https://github.com/J3vb/OwnCord/releases/download/v1.0.0/chatserver.exe"
|
||||
checksumURL := "https://github.com/J3vb/OwnCord/releases/download/v1.0.0/checksums.sha256"
|
||||
|
||||
err := u.DownloadAndVerify(context.Background(), downloadURL, checksumURL, dest)
|
||||
if err == nil {
|
||||
t.Error("DownloadAndVerify should fail on checksum mismatch")
|
||||
}
|
||||
|
||||
// File should be removed after mismatch.
|
||||
if _, statErr := os.Stat(dest); !os.IsNotExist(statErr) {
|
||||
t.Error("file should be removed after checksum mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
// rewriteTransport rewrites GitHub release URLs to a local test server.
|
||||
type rewriteTransport struct {
|
||||
target string
|
||||
}
|
||||
|
||||
func (rt *rewriteTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// Rewrite github.com URLs to the test server.
|
||||
newURL := rt.target + "/download/" + filepath.Base(req.URL.Path)
|
||||
newReq, _ := http.NewRequestWithContext(req.Context(), req.Method, newURL, req.Body)
|
||||
return http.DefaultTransport.RoundTrip(newReq)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -51,3 +51,13 @@ func BuildVoiceOfferForTest(channelID int64, sdp string) []byte {
|
||||
func BuildVoiceICEForTest(channelID int64, candidate any) []byte {
|
||||
return buildVoiceICE(channelID, candidate)
|
||||
}
|
||||
|
||||
// SetupICECallbackForTest exposes setupICECallback for external tests.
|
||||
func (h *Hub) SetupICECallbackForTest(c *Client, channelID int64) {
|
||||
h.setupICECallback(c, channelID)
|
||||
}
|
||||
|
||||
// RenegotiateParticipantForTest exposes renegotiateParticipant for external tests.
|
||||
func (h *Hub) RenegotiateParticipantForTest(c *Client) {
|
||||
h.renegotiateParticipant(c)
|
||||
}
|
||||
|
||||
@@ -226,3 +226,68 @@ func TestParseAudioLevel_NotFound(t *testing.T) {
|
||||
t.Error("expected ok=false for wrong extension ID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAudioLevel_PaddingByte(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Padding byte (ID=0), then actual extension ID=1.
|
||||
// Padding: byte 0x00 (id=0 means skip)
|
||||
// Extension: ID=1, L=0 (1 byte), data=0x8A (voice=1, level=10)
|
||||
buf := []byte{0x00, 1 << 4, 0x8A}
|
||||
|
||||
level, voice, ok := ws.ParseAudioLevel(buf, 1)
|
||||
if !ok {
|
||||
t.Fatal("expected ok=true after padding byte")
|
||||
}
|
||||
if level != 10 {
|
||||
t.Errorf("level = %d, want 10", level)
|
||||
}
|
||||
if !voice {
|
||||
t.Error("expected voice=true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAudioLevel_Terminator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Terminator byte (ID=15) before any matching extension.
|
||||
buf := []byte{0xF0} // ID=15, terminates
|
||||
|
||||
_, _, ok := ws.ParseAudioLevel(buf, 1)
|
||||
if ok {
|
||||
t.Error("expected ok=false when terminator encountered before matching ID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAudioLevel_TruncatedData(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Extension header says 1 byte of data, but buffer ends before data.
|
||||
// ID=1, L=0 (meaning 1 byte of data needed), but no data follows.
|
||||
buf := []byte{1 << 4}
|
||||
|
||||
_, _, ok := ws.ParseAudioLevel(buf, 1)
|
||||
if ok {
|
||||
t.Error("expected ok=false when data is truncated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAudioLevel_SkipOtherExtension(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Extension ID=2 with 2 bytes of data, followed by ID=1 with actual data.
|
||||
// ID=2, L=1 (2 bytes data): header 0x21, data 0x00 0x00
|
||||
// ID=1, L=0 (1 byte data): header 0x10, data 0x85 (voice=1, level=5)
|
||||
buf := []byte{0x21, 0x00, 0x00, 0x10, 0x85}
|
||||
|
||||
level, voice, ok := ws.ParseAudioLevel(buf, 1)
|
||||
if !ok {
|
||||
t.Fatal("expected ok=true after skipping other extension")
|
||||
}
|
||||
if level != 5 {
|
||||
t.Errorf("level = %d, want 5", level)
|
||||
}
|
||||
if !voice {
|
||||
t.Error("expected voice=true")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user