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>
786 lines
27 KiB
Go
786 lines
27 KiB
Go
package db_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/owncord/server/db"
|
|
)
|
|
|
|
// ─── JoinVoiceChannelIfCapacity ─────────────────────────────────────────────
|
|
|
|
func TestVoice_JoinVoiceChannelIfCapacity_UnderLimit(t *testing.T) {
|
|
database := newVoiceTestDB(t)
|
|
u1 := seedVoiceUser(t, database, "cap-u1")
|
|
chanID := seedVoiceChannel(t, database, "cap-ch")
|
|
|
|
err := database.JoinVoiceChannelIfCapacity(context.Background(), u1, chanID, 2)
|
|
if err != nil {
|
|
t.Fatalf("JoinVoiceChannelIfCapacity: %v", err)
|
|
}
|
|
|
|
state, err := database.GetVoiceState(context.Background(), u1)
|
|
if err != nil {
|
|
t.Fatalf("GetVoiceState: %v", err)
|
|
}
|
|
if state == nil {
|
|
t.Fatal("expected voice state after join")
|
|
}
|
|
if state.ChannelID != chanID {
|
|
t.Errorf("ChannelID = %d, want %d", state.ChannelID, chanID)
|
|
}
|
|
}
|
|
|
|
func TestVoice_JoinVoiceChannelIfCapacity_AtLimit(t *testing.T) {
|
|
database := newVoiceTestDB(t)
|
|
u1 := seedVoiceUser(t, database, "cap-full1")
|
|
u2 := seedVoiceUser(t, database, "cap-full2")
|
|
u3 := seedVoiceUser(t, database, "cap-full3")
|
|
chanID := seedVoiceChannel(t, database, "cap-full-ch")
|
|
|
|
// Fill channel to capacity (max 2).
|
|
if err := database.JoinVoiceChannelIfCapacity(context.Background(), u1, chanID, 2); err != nil {
|
|
t.Fatalf("first join: %v", err)
|
|
}
|
|
if err := database.JoinVoiceChannelIfCapacity(context.Background(), u2, chanID, 2); err != nil {
|
|
t.Fatalf("second join: %v", err)
|
|
}
|
|
|
|
// Third join should fail with ErrChannelFull.
|
|
err := database.JoinVoiceChannelIfCapacity(context.Background(), u3, chanID, 2)
|
|
if err == nil {
|
|
t.Fatal("expected ErrChannelFull, got nil")
|
|
}
|
|
if err != db.ErrChannelFull {
|
|
t.Errorf("error = %v, want ErrChannelFull", err)
|
|
}
|
|
}
|
|
|
|
func TestVoice_JoinVoiceChannelIfCapacity_ReplacesOwnState(t *testing.T) {
|
|
database := newVoiceTestDB(t)
|
|
u1 := seedVoiceUser(t, database, "cap-replace")
|
|
ch1 := seedVoiceChannel(t, database, "cap-ch1")
|
|
ch2 := seedVoiceChannel(t, database, "cap-ch2")
|
|
|
|
// Join ch1, then join ch2 with capacity check — should replace.
|
|
if err := database.JoinVoiceChannelIfCapacity(context.Background(), u1, ch1, 5); err != nil {
|
|
t.Fatalf("join ch1: %v", err)
|
|
}
|
|
if err := database.JoinVoiceChannelIfCapacity(context.Background(), u1, ch2, 5); err != nil {
|
|
t.Fatalf("join ch2: %v", err)
|
|
}
|
|
|
|
state, _ := database.GetVoiceState(context.Background(), u1)
|
|
if state == nil || state.ChannelID != ch2 {
|
|
t.Errorf("expected channel %d, got %v", ch2, state)
|
|
}
|
|
}
|
|
|
|
// ─── GetAllVoiceStates ──────────────────────────────────────────────────────
|
|
|
|
func TestVoice_GetAllVoiceStates_Empty(t *testing.T) {
|
|
database := newVoiceTestDB(t)
|
|
|
|
states, err := database.GetAllVoiceStates(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("GetAllVoiceStates: %v", err)
|
|
}
|
|
if len(states) != 0 {
|
|
t.Errorf("got %d states, want 0", len(states))
|
|
}
|
|
}
|
|
|
|
func TestVoice_GetAllVoiceStates_MultipleChannels(t *testing.T) {
|
|
database := newVoiceTestDB(t)
|
|
u1 := seedVoiceUser(t, database, "all-vs-u1")
|
|
u2 := seedVoiceUser(t, database, "all-vs-u2")
|
|
u3 := seedVoiceUser(t, database, "all-vs-u3")
|
|
ch1 := seedVoiceChannel(t, database, "all-vs-ch1")
|
|
ch2 := seedVoiceChannel(t, database, "all-vs-ch2")
|
|
|
|
_ = database.JoinVoiceChannel(context.Background(), u1, ch1)
|
|
_ = database.JoinVoiceChannel(context.Background(), u2, ch1)
|
|
_ = database.JoinVoiceChannel(context.Background(), u3, ch2)
|
|
|
|
states, err := database.GetAllVoiceStates(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("GetAllVoiceStates: %v", err)
|
|
}
|
|
if len(states) != 3 {
|
|
t.Errorf("got %d states, want 3", len(states))
|
|
}
|
|
}
|
|
|
|
// ─── CountActiveCameras ─────────────────────────────────────────────────────
|
|
|
|
func TestVoice_CountActiveCameras_Zero(t *testing.T) {
|
|
database := newVoiceTestDB(t)
|
|
chanID := seedVoiceChannel(t, database, "cam-count-empty")
|
|
|
|
count, err := database.CountActiveCameras(context.Background(), chanID)
|
|
if err != nil {
|
|
t.Fatalf("CountActiveCameras: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Errorf("count = %d, want 0", count)
|
|
}
|
|
}
|
|
|
|
func TestVoice_CountActiveCameras_SomeCameras(t *testing.T) {
|
|
database := newVoiceTestDB(t)
|
|
u1 := seedVoiceUser(t, database, "cam-cnt-u1")
|
|
u2 := seedVoiceUser(t, database, "cam-cnt-u2")
|
|
u3 := seedVoiceUser(t, database, "cam-cnt-u3")
|
|
chanID := seedVoiceChannel(t, database, "cam-cnt-ch")
|
|
|
|
_ = database.JoinVoiceChannel(context.Background(), u1, chanID)
|
|
_ = database.JoinVoiceChannel(context.Background(), u2, chanID)
|
|
_ = database.JoinVoiceChannel(context.Background(), u3, chanID)
|
|
|
|
_ = database.UpdateVoiceCamera(context.Background(), u1, true)
|
|
_ = database.UpdateVoiceCamera(context.Background(), u2, true)
|
|
// u3 camera stays off.
|
|
|
|
count, err := database.CountActiveCameras(context.Background(), chanID)
|
|
if err != nil {
|
|
t.Fatalf("CountActiveCameras: %v", err)
|
|
}
|
|
if count != 2 {
|
|
t.Errorf("count = %d, want 2", count)
|
|
}
|
|
}
|
|
|
|
// ─── EnableCameraIfUnderLimit ───────────────────────────────────────────────
|
|
|
|
func TestVoice_EnableCameraIfUnderLimit_Success(t *testing.T) {
|
|
database := newVoiceTestDB(t)
|
|
u1 := seedVoiceUser(t, database, "cam-limit-ok")
|
|
chanID := seedVoiceChannel(t, database, "cam-limit-ch")
|
|
|
|
_ = database.JoinVoiceChannel(context.Background(), u1, chanID)
|
|
|
|
ok, err := database.EnableCameraIfUnderLimit(context.Background(), u1, chanID, 2)
|
|
if err != nil {
|
|
t.Fatalf("EnableCameraIfUnderLimit: %v", err)
|
|
}
|
|
if !ok {
|
|
t.Error("expected camera to be enabled")
|
|
}
|
|
|
|
state, _ := database.GetVoiceState(context.Background(), u1)
|
|
if state == nil || !state.Camera {
|
|
t.Error("camera should be true after enable")
|
|
}
|
|
}
|
|
|
|
func TestVoice_EnableCameraIfUnderLimit_AtLimit(t *testing.T) {
|
|
database := newVoiceTestDB(t)
|
|
u1 := seedVoiceUser(t, database, "cam-lim-u1")
|
|
u2 := seedVoiceUser(t, database, "cam-lim-u2")
|
|
u3 := seedVoiceUser(t, database, "cam-lim-u3")
|
|
chanID := seedVoiceChannel(t, database, "cam-lim-ch")
|
|
|
|
_ = database.JoinVoiceChannel(context.Background(), u1, chanID)
|
|
_ = database.JoinVoiceChannel(context.Background(), u2, chanID)
|
|
_ = database.JoinVoiceChannel(context.Background(), u3, chanID)
|
|
|
|
// Enable cameras for u1 and u2 (max is 2).
|
|
_, _ = database.EnableCameraIfUnderLimit(context.Background(), u1, chanID, 2)
|
|
_, _ = database.EnableCameraIfUnderLimit(context.Background(), u2, chanID, 2)
|
|
|
|
// u3 should be denied.
|
|
ok, err := database.EnableCameraIfUnderLimit(context.Background(), u3, chanID, 2)
|
|
if err != nil {
|
|
t.Fatalf("EnableCameraIfUnderLimit: %v", err)
|
|
}
|
|
if ok {
|
|
t.Error("expected camera to be denied at limit")
|
|
}
|
|
}
|
|
|
|
// ─── SearchMessagesInChannels ───────────────────────────────────────────────
|
|
|
|
func TestSearchMessagesInChannels_FindsInAllowedChannels(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "srch-multi")
|
|
ch1 := seedChannel(t, database, "srch-ch1")
|
|
ch2 := seedChannel(t, database, "srch-ch2")
|
|
ch3 := seedChannel(t, database, "srch-ch3")
|
|
|
|
_, _ = database.CreateMessage(context.Background(), ch1, userID, "alpha keyword here", nil)
|
|
_, _ = database.CreateMessage(context.Background(), ch2, userID, "beta keyword here", nil)
|
|
_, _ = database.CreateMessage(context.Background(), ch3, userID, "gamma keyword here", nil)
|
|
|
|
// Search only in ch1 and ch2.
|
|
results, err := database.SearchMessagesInChannels(context.Background(), "keyword", []int64{ch1, ch2}, 10)
|
|
if err != nil {
|
|
t.Fatalf("SearchMessagesInChannels: %v", err)
|
|
}
|
|
if len(results) != 2 {
|
|
t.Errorf("expected 2 results, got %d", len(results))
|
|
}
|
|
for _, r := range results {
|
|
if r.ChannelID != ch1 && r.ChannelID != ch2 {
|
|
t.Errorf("unexpected channel_id %d in results", r.ChannelID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSearchMessagesInChannels_EmptyQuery(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
|
|
results, err := database.SearchMessagesInChannels(context.Background(), "", []int64{1}, 10)
|
|
if err != nil {
|
|
t.Fatalf("SearchMessagesInChannels: %v", err)
|
|
}
|
|
if len(results) != 0 {
|
|
t.Errorf("expected 0 results for empty query, got %d", len(results))
|
|
}
|
|
}
|
|
|
|
func TestSearchMessagesInChannels_EmptyChannelIDs(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
|
|
results, err := database.SearchMessagesInChannels(context.Background(), "test", nil, 10)
|
|
if err != nil {
|
|
t.Fatalf("SearchMessagesInChannels: %v", err)
|
|
}
|
|
if len(results) != 0 {
|
|
t.Errorf("expected 0 results for no channels, got %d", len(results))
|
|
}
|
|
}
|
|
|
|
func TestSearchMessagesInChannels_LimitRespected(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "srch-lim")
|
|
ch1 := seedChannel(t, database, "srch-lim-ch")
|
|
|
|
for range 5 {
|
|
_, _ = database.CreateMessage(context.Background(), ch1, userID, "findme content here", nil)
|
|
}
|
|
|
|
results, err := database.SearchMessagesInChannels(context.Background(), "findme", []int64{ch1}, 2)
|
|
if err != nil {
|
|
t.Fatalf("SearchMessagesInChannels: %v", err)
|
|
}
|
|
if len(results) != 2 {
|
|
t.Errorf("expected 2 results (limit), got %d", len(results))
|
|
}
|
|
}
|
|
|
|
func TestSearchMessagesInChannels_ZeroLimit(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
|
|
results, err := database.SearchMessagesInChannels(context.Background(), "test", []int64{1}, 0)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(results) != 0 {
|
|
t.Errorf("expected 0 results for zero limit, got %d", len(results))
|
|
}
|
|
}
|
|
|
|
// ─── GetPinnedMessages ──────────────────────────────────────────────────────
|
|
|
|
func TestGetPinnedMessages_Empty(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "pin-empty-u")
|
|
chID := seedChannel(t, database, "pin-empty")
|
|
|
|
msgs, err := database.GetPinnedMessages(context.Background(), chID, userID)
|
|
if err != nil {
|
|
t.Fatalf("GetPinnedMessages: %v", err)
|
|
}
|
|
if len(msgs) != 0 {
|
|
t.Errorf("expected 0 pinned messages, got %d", len(msgs))
|
|
}
|
|
}
|
|
|
|
func TestGetPinnedMessages_ReturnsPinnedOnly(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "pin-user")
|
|
chID := seedChannel(t, database, "pin-ch")
|
|
|
|
id1, _ := database.CreateMessage(context.Background(), chID, userID, "pinned msg", nil)
|
|
_, _ = database.CreateMessage(context.Background(), chID, userID, "not pinned", nil)
|
|
_ = database.SetMessagePinned(context.Background(), id1, true)
|
|
|
|
msgs, err := database.GetPinnedMessages(context.Background(), chID, userID)
|
|
if err != nil {
|
|
t.Fatalf("GetPinnedMessages: %v", err)
|
|
}
|
|
if len(msgs) != 1 {
|
|
t.Fatalf("expected 1 pinned message, got %d", len(msgs))
|
|
}
|
|
if msgs[0].Content != "pinned msg" {
|
|
t.Errorf("Content = %q, want 'pinned msg'", msgs[0].Content)
|
|
}
|
|
if !msgs[0].Pinned {
|
|
t.Error("expected Pinned=true")
|
|
}
|
|
}
|
|
|
|
// ─── SetMessagePinned ───────────────────────────────────────────────────────
|
|
|
|
func TestSetMessagePinned_Pin(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "setpin-u")
|
|
chID := seedChannel(t, database, "setpin-ch")
|
|
|
|
id, _ := database.CreateMessage(context.Background(), chID, userID, "to pin", nil)
|
|
|
|
if err := database.SetMessagePinned(context.Background(), id, true); err != nil {
|
|
t.Fatalf("SetMessagePinned(true): %v", err)
|
|
}
|
|
|
|
msg, _ := database.GetMessage(context.Background(), id)
|
|
if msg == nil || !msg.Pinned {
|
|
t.Error("message should be pinned")
|
|
}
|
|
}
|
|
|
|
func TestSetMessagePinned_Unpin(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "unpin-u")
|
|
chID := seedChannel(t, database, "unpin-ch")
|
|
|
|
id, _ := database.CreateMessage(context.Background(), chID, userID, "to unpin", nil)
|
|
_ = database.SetMessagePinned(context.Background(), id, true)
|
|
if err := database.SetMessagePinned(context.Background(), id, false); err != nil {
|
|
t.Fatalf("SetMessagePinned(false): %v", err)
|
|
}
|
|
|
|
msg, _ := database.GetMessage(context.Background(), id)
|
|
if msg == nil || msg.Pinned {
|
|
t.Error("message should not be pinned")
|
|
}
|
|
}
|
|
|
|
func TestSetMessagePinned_NotFound(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
|
|
err := database.SetMessagePinned(context.Background(), 99999, true)
|
|
if err == nil {
|
|
t.Error("expected error for non-existent message")
|
|
}
|
|
}
|
|
|
|
func TestSetMessagePinned_DeletedMessage(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "pin-del-u")
|
|
chID := seedChannel(t, database, "pin-del-ch")
|
|
|
|
id, _ := database.CreateMessage(context.Background(), chID, userID, "deleted", nil)
|
|
_ = database.DeleteMessage(context.Background(), id, userID, false)
|
|
|
|
err := database.SetMessagePinned(context.Background(), id, true)
|
|
if err == nil {
|
|
t.Error("expected error when pinning deleted message")
|
|
}
|
|
}
|
|
|
|
// ─── CreateAttachment ───────────────────────────────────────────────────────
|
|
|
|
func TestCreateAttachment_Success(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "att-uploader")
|
|
|
|
err := database.CreateAttachment(context.Background(), "att-001", userID, "photo.png", "stored-001.png", "image/png", 12345, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("CreateAttachment: %v", err)
|
|
}
|
|
|
|
att, err := database.GetAttachmentByID(context.Background(), "att-001")
|
|
if err != nil {
|
|
t.Fatalf("GetAttachmentByID: %v", err)
|
|
}
|
|
if att == nil {
|
|
t.Fatal("expected attachment, got nil")
|
|
}
|
|
if att.Filename != "photo.png" {
|
|
t.Errorf("Filename = %q, want 'photo.png'", att.Filename)
|
|
}
|
|
if att.Size != 12345 {
|
|
t.Errorf("Size = %d, want 12345", att.Size)
|
|
}
|
|
if att.MimeType != "image/png" {
|
|
t.Errorf("MimeType = %q, want 'image/png'", att.MimeType)
|
|
}
|
|
}
|
|
|
|
func TestCreateAttachment_WithDimensions(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "att-dim-uploader")
|
|
|
|
w, h := 1920, 1080
|
|
err := database.CreateAttachment(context.Background(), "att-dim", userID, "photo.jpg", "stored-dim.jpg", "image/jpeg", 54321, &w, &h)
|
|
if err != nil {
|
|
t.Fatalf("CreateAttachment with dims: %v", err)
|
|
}
|
|
|
|
att, _ := database.GetAttachmentByID(context.Background(), "att-dim")
|
|
if att == nil {
|
|
t.Fatal("expected attachment")
|
|
}
|
|
}
|
|
|
|
// ─── DeleteOrphanedAttachments ──────────────────────────────────────────────
|
|
|
|
func TestDeleteOrphanedAttachments_RemovesOrphans(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "orphan-uploader")
|
|
|
|
// Create an unlinked attachment (message_id IS NULL).
|
|
_ = database.CreateAttachment(context.Background(), "orphan-1", userID, "file.txt", "stored-orphan.txt", "text/plain", 100, nil, nil)
|
|
|
|
// Use a cutoff far in the future so the attachment is considered old.
|
|
files, err := database.DeleteOrphanedAttachments(context.Background(), "2099-01-01T00:00:00Z")
|
|
if err != nil {
|
|
t.Fatalf("DeleteOrphanedAttachments: %v", err)
|
|
}
|
|
if len(files) != 1 {
|
|
t.Fatalf("expected 1 orphan, got %d", len(files))
|
|
}
|
|
if files[0] != "stored-orphan.txt" {
|
|
t.Errorf("stored_as = %q, want 'stored-orphan.txt'", files[0])
|
|
}
|
|
|
|
// Should be removed from DB.
|
|
att, _ := database.GetAttachmentByID(context.Background(), "orphan-1")
|
|
if att != nil {
|
|
t.Error("orphaned attachment should be deleted from DB")
|
|
}
|
|
}
|
|
|
|
func TestDeleteOrphanedAttachments_KeepsLinked(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "orphan-linked-u")
|
|
chID := seedChannel(t, database, "orphan-linked-ch")
|
|
|
|
// Create attachment and link it to a message.
|
|
_ = database.CreateAttachment(context.Background(), "linked-1", userID, "file.txt", "stored-linked.txt", "text/plain", 100, nil, nil)
|
|
msgID, _ := database.CreateMessage(context.Background(), chID, userID, "with attachment", nil)
|
|
_, _ = database.LinkAttachmentsToMessage(context.Background(), msgID, userID, []string{"linked-1"})
|
|
|
|
files, err := database.DeleteOrphanedAttachments(context.Background(), "2099-01-01T00:00:00Z")
|
|
if err != nil {
|
|
t.Fatalf("DeleteOrphanedAttachments: %v", err)
|
|
}
|
|
if len(files) != 0 {
|
|
t.Errorf("expected 0 orphans (linked), got %d", len(files))
|
|
}
|
|
}
|
|
|
|
func TestDeleteOrphanedAttachments_CutoffRespected(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "cutoff-uploader")
|
|
|
|
_ = database.CreateAttachment(context.Background(), "future-1", userID, "file.txt", "stored-future.txt", "text/plain", 100, nil, nil)
|
|
|
|
// Cutoff in the past — newly created attachment should NOT be deleted.
|
|
files, err := database.DeleteOrphanedAttachments(context.Background(), "2000-01-01T00:00:00Z")
|
|
if err != nil {
|
|
t.Fatalf("DeleteOrphanedAttachments: %v", err)
|
|
}
|
|
if len(files) != 0 {
|
|
t.Errorf("expected 0 orphans (cutoff too old), got %d", len(files))
|
|
}
|
|
}
|
|
|
|
// ─── GetAllChannelPermissionsForRole ────────────────────────────────────────
|
|
|
|
func TestGetAllChannelPermissionsForRole_Empty(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
|
|
result, err := database.GetAllChannelPermissionsForRole(context.Background(), 4)
|
|
if err != nil {
|
|
t.Fatalf("GetAllChannelPermissionsForRole: %v", err)
|
|
}
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty map, got %d entries", len(result))
|
|
}
|
|
}
|
|
|
|
func TestGetAllChannelPermissionsForRole_WithOverrides(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
|
|
ch1, _ := database.CreateChannel(context.Background(), "perm-ch1", "text", "", "", 0)
|
|
ch2, _ := database.CreateChannel(context.Background(), "perm-ch2", "text", "", "", 0)
|
|
|
|
// Insert overrides for role 4.
|
|
_, _ = database.ExecContext(context.Background(),
|
|
`INSERT INTO channel_overrides (channel_id, role_id, allow, deny) VALUES (?, ?, ?, ?)`,
|
|
ch1, 4, int64(0x100), int64(0x200),
|
|
)
|
|
_, _ = database.ExecContext(context.Background(),
|
|
`INSERT INTO channel_overrides (channel_id, role_id, allow, deny) VALUES (?, ?, ?, ?)`,
|
|
ch2, 4, int64(0x300), int64(0),
|
|
)
|
|
|
|
result, err := database.GetAllChannelPermissionsForRole(context.Background(), 4)
|
|
if err != nil {
|
|
t.Fatalf("GetAllChannelPermissionsForRole: %v", err)
|
|
}
|
|
if len(result) != 2 {
|
|
t.Fatalf("expected 2 entries, got %d", len(result))
|
|
}
|
|
if o, ok := result[ch1]; !ok || o.Allow != 0x100 || o.Deny != 0x200 {
|
|
t.Errorf("ch1 override = %+v, want allow=0x100 deny=0x200", result[ch1])
|
|
}
|
|
}
|
|
|
|
// ─── GetChannelTypes ────────────────────────────────────────────────────────
|
|
|
|
func TestGetChannelTypes_Empty(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
|
|
result, err := database.GetChannelTypes(context.Background(), nil)
|
|
if err != nil {
|
|
t.Fatalf("GetChannelTypes: %v", err)
|
|
}
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty map, got %d", len(result))
|
|
}
|
|
}
|
|
|
|
func TestGetChannelTypes_ReturnsTypes(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
|
|
ch1, _ := database.CreateChannel(context.Background(), "type-text", "text", "", "", 0)
|
|
ch2, _ := database.CreateChannel(context.Background(), "type-voice", "voice", "", "", 0)
|
|
|
|
result, err := database.GetChannelTypes(context.Background(), []int64{ch1, ch2})
|
|
if err != nil {
|
|
t.Fatalf("GetChannelTypes: %v", err)
|
|
}
|
|
if result[ch1] != "text" {
|
|
t.Errorf("ch1 type = %q, want 'text'", result[ch1])
|
|
}
|
|
if result[ch2] != "voice" {
|
|
t.Errorf("ch2 type = %q, want 'voice'", result[ch2])
|
|
}
|
|
}
|
|
|
|
func TestGetChannelTypes_NonExistentIDs(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
|
|
result, err := database.GetChannelTypes(context.Background(), []int64{99999})
|
|
if err != nil {
|
|
t.Fatalf("GetChannelTypes: %v", err)
|
|
}
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty map for non-existent IDs, got %d", len(result))
|
|
}
|
|
}
|
|
|
|
// ─── CountUsersWithoutTOTP ──────────────────────────────────────────────────
|
|
|
|
func TestCountUsersWithoutTOTP_AllWithout(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
_, _ = database.CreateUser(context.Background(), "totp-u1", "hash", 4)
|
|
_, _ = database.CreateUser(context.Background(), "totp-u2", "hash", 4)
|
|
|
|
count, err := database.CountUsersWithoutTOTP(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("CountUsersWithoutTOTP: %v", err)
|
|
}
|
|
if count != 2 {
|
|
t.Errorf("count = %d, want 2", count)
|
|
}
|
|
}
|
|
|
|
func TestCountUsersWithoutTOTP_WithTOTPSetup(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
uid, _ := database.CreateUser(context.Background(), "totp-with", "hash", 4)
|
|
_, _ = database.CreateUser(context.Background(), "totp-without", "hash", 4)
|
|
|
|
secret := "JBSWY3DPEHPK3PXP"
|
|
_ = database.UpdateUserTOTPSecret(context.Background(), uid, &secret)
|
|
|
|
count, err := database.CountUsersWithoutTOTP(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("CountUsersWithoutTOTP: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Errorf("count = %d, want 1 (one has TOTP)", count)
|
|
}
|
|
}
|
|
|
|
// ─── UpdateUserTOTPSecret ───────────────────────────────────────────────────
|
|
|
|
func TestUpdateUserTOTPSecret_Set(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
uid, _ := database.CreateUser(context.Background(), "totp-set", "hash", 4)
|
|
|
|
secret := "JBSWY3DPEHPK3PXP"
|
|
if err := database.UpdateUserTOTPSecret(context.Background(), uid, &secret); err != nil {
|
|
t.Fatalf("UpdateUserTOTPSecret(set): %v", err)
|
|
}
|
|
|
|
user, _ := database.GetUserByID(context.Background(), uid)
|
|
if user == nil || user.TOTPSecret == nil || *user.TOTPSecret != secret {
|
|
t.Error("TOTP secret should be set")
|
|
}
|
|
}
|
|
|
|
func TestUpdateUserTOTPSecret_Clear(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
uid, _ := database.CreateUser(context.Background(), "totp-clear", "hash", 4)
|
|
|
|
secret := "JBSWY3DPEHPK3PXP"
|
|
_ = database.UpdateUserTOTPSecret(context.Background(), uid, &secret)
|
|
if err := database.UpdateUserTOTPSecret(context.Background(), uid, nil); err != nil {
|
|
t.Fatalf("UpdateUserTOTPSecret(clear): %v", err)
|
|
}
|
|
|
|
user, _ := database.GetUserByID(context.Background(), uid)
|
|
if user == nil || user.TOTPSecret != nil {
|
|
t.Error("TOTP secret should be nil after clear")
|
|
}
|
|
}
|
|
|
|
// ─── CreateUserWithInvite ───────────────────────────────────────────────────
|
|
|
|
func TestCreateUserWithInvite_Success(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
// Create a user who will create the invite.
|
|
creatorID, _ := database.CreateUser(context.Background(), "invite-creator", "hash", 2)
|
|
|
|
code, err := database.CreateInvite(context.Background(), creatorID, 5, nil)
|
|
if err != nil {
|
|
t.Fatalf("CreateInvite: %v", err)
|
|
}
|
|
|
|
uid, err := database.CreateUserWithInvite(context.Background(), "newuser", "hash", 4, code)
|
|
if err != nil {
|
|
t.Fatalf("CreateUserWithInvite: %v", err)
|
|
}
|
|
if uid <= 0 {
|
|
t.Errorf("expected positive user ID, got %d", uid)
|
|
}
|
|
|
|
// Verify invite use count incremented.
|
|
inv, _ := database.GetInvite(context.Background(), code)
|
|
if inv == nil || inv.Uses != 1 {
|
|
t.Errorf("invite uses = %v, want 1", inv)
|
|
}
|
|
}
|
|
|
|
func TestCreateUserWithInvite_InvalidCode(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
|
|
_, err := database.CreateUserWithInvite(context.Background(), "baduser", "hash", 4, "nonexistent-code")
|
|
if err == nil {
|
|
t.Error("expected error for invalid invite code")
|
|
}
|
|
}
|
|
|
|
func TestCreateUserWithInvite_RevokedInvite(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
creatorID, _ := database.CreateUser(context.Background(), "inv-revoke-creator", "hash", 2)
|
|
|
|
code, _ := database.CreateInvite(context.Background(), creatorID, 0, nil)
|
|
_ = database.RevokeInvite(context.Background(), code)
|
|
|
|
_, err := database.CreateUserWithInvite(context.Background(), "revokeduser", "hash", 4, code)
|
|
if err == nil {
|
|
t.Error("expected error for revoked invite")
|
|
}
|
|
}
|
|
|
|
func TestCreateUserWithInvite_ExpiredInvite(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
creatorID, _ := database.CreateUser(context.Background(), "inv-expire-creator", "hash", 2)
|
|
|
|
// Create an invite that expires in the past.
|
|
pastTime := time.Now().Add(-1 * time.Hour)
|
|
code, _ := database.CreateInvite(context.Background(), creatorID, 0, &pastTime)
|
|
|
|
_, err := database.CreateUserWithInvite(context.Background(), "expireduser", "hash", 4, code)
|
|
if err == nil {
|
|
t.Error("expected error for expired invite")
|
|
}
|
|
}
|
|
|
|
// ─── ListInvites (db layer) ─────────────────────────────────────────────────
|
|
|
|
func TestListInvites_DB_Empty(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
|
|
invites, err := database.ListInvites(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("ListInvites: %v", err)
|
|
}
|
|
if len(invites) != 0 {
|
|
t.Errorf("expected 0 invites, got %d", len(invites))
|
|
}
|
|
}
|
|
|
|
func TestListInvites_DB_ReturnsAll(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
creatorID, _ := database.CreateUser(context.Background(), "list-inv-creator", "hash", 2)
|
|
|
|
_, _ = database.CreateInvite(context.Background(), creatorID, 5, nil)
|
|
_, _ = database.CreateInvite(context.Background(), creatorID, 0, nil)
|
|
|
|
invites, err := database.ListInvites(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("ListInvites: %v", err)
|
|
}
|
|
if len(invites) != 2 {
|
|
t.Errorf("expected 2 invites, got %d", len(invites))
|
|
}
|
|
}
|
|
|
|
func TestUseInviteAtomic_NonExistent(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
|
|
err := database.UseInviteAtomic(context.Background(), "does-not-exist")
|
|
if err == nil {
|
|
t.Error("expected error for non-existent invite")
|
|
}
|
|
}
|
|
|
|
// ─── SearchMessages edge cases ──────────────────────────────────────────────
|
|
|
|
func TestSearchMessages_EmptyQuery(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
|
|
results, err := database.SearchMessages(context.Background(), "", nil, 10)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(results) != 0 {
|
|
t.Errorf("expected 0 results for empty query, got %d", len(results))
|
|
}
|
|
}
|
|
|
|
func TestSearchMessages_ZeroLimit(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
|
|
results, err := database.SearchMessages(context.Background(), "test", nil, 0)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(results) != 0 {
|
|
t.Errorf("expected 0 results for zero limit, got %d", len(results))
|
|
}
|
|
}
|
|
|
|
func TestSearchMessages_SpecialCharsStripped(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "srch-special")
|
|
chID := seedChannel(t, database, "srch-special-ch")
|
|
|
|
_, _ = database.CreateMessage(context.Background(), chID, userID, "hello world content", nil)
|
|
|
|
// FTS special chars should be stripped, leaving a valid query.
|
|
results, err := database.SearchMessages(context.Background(), "hello* \"world\"", nil, 10)
|
|
if err != nil {
|
|
t.Fatalf("SearchMessages with special chars: %v", err)
|
|
}
|
|
// Should not crash, results may vary.
|
|
_ = results
|
|
}
|