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:
jevb
2026-03-18 05:03:38 +01:00
parent 8a85386480
commit fca1b0cc0c
10 changed files with 3506 additions and 0 deletions
+98
View File
@@ -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")
}
}
+178
View File
@@ -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")
}
}
}
+131
View File
@@ -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)
}
}
+58
View File
@@ -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)
}
}
+200
View File
@@ -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)
}
}
+87
View File
@@ -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")
}
}
+216
View 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
+10
View File
@@ -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)
}
+65
View File
@@ -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")
}
}