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>
725 lines
24 KiB
Go
725 lines
24 KiB
Go
package db_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/owncord/server/db"
|
|
)
|
|
|
|
// seedUser inserts a minimal test user and returns its ID.
|
|
func seedUser(t *testing.T, database *db.DB, username string) int64 {
|
|
t.Helper()
|
|
id, err := database.CreateUser(context.Background(), username, "hash", 4)
|
|
if err != nil {
|
|
t.Fatalf("seedUser(%q): %v", username, err)
|
|
}
|
|
return id
|
|
}
|
|
|
|
// seedChannel inserts a minimal test channel and returns its ID.
|
|
func seedChannel(t *testing.T, database *db.DB, name string) int64 {
|
|
t.Helper()
|
|
id, err := database.CreateChannel(context.Background(), name, "text", "", "", 0)
|
|
if err != nil {
|
|
t.Fatalf("seedChannel(%q): %v", name, err)
|
|
}
|
|
return id
|
|
}
|
|
|
|
// ─── CreateMessage ────────────────────────────────────────────────────────────
|
|
|
|
func TestCreateMessage_ReturnsID(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "alice")
|
|
chID := seedChannel(t, database, "general")
|
|
|
|
id, err := database.CreateMessage(context.Background(), chID, userID, "hello", nil)
|
|
if err != nil {
|
|
t.Fatalf("CreateMessage: %v", err)
|
|
}
|
|
if id <= 0 {
|
|
t.Errorf("expected positive ID, got %d", id)
|
|
}
|
|
}
|
|
|
|
func TestCreateMessage_WithReplyTo(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "alice")
|
|
chID := seedChannel(t, database, "general")
|
|
|
|
parentID, _ := database.CreateMessage(context.Background(), chID, userID, "parent", nil)
|
|
replyID, err := database.CreateMessage(context.Background(), chID, userID, "reply", &parentID)
|
|
if err != nil {
|
|
t.Fatalf("CreateMessage with reply: %v", err)
|
|
}
|
|
|
|
msg, _ := database.GetMessage(context.Background(), replyID)
|
|
if msg.ReplyTo == nil || *msg.ReplyTo != parentID {
|
|
t.Errorf("ReplyTo = %v, want %d", msg.ReplyTo, parentID)
|
|
}
|
|
}
|
|
|
|
func TestCreateMessage_ContentPreserved(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "bob")
|
|
chID := seedChannel(t, database, "ch")
|
|
|
|
id, _ := database.CreateMessage(context.Background(), chID, userID, "test content", nil)
|
|
msg, _ := database.GetMessage(context.Background(), id)
|
|
if msg.Content != "test content" {
|
|
t.Errorf("Content = %q, want 'test content'", msg.Content)
|
|
}
|
|
}
|
|
|
|
// ─── GetMessage ───────────────────────────────────────────────────────────────
|
|
|
|
func TestGetMessage_NotFound(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
|
|
msg, err := database.GetMessage(context.Background(), 9999)
|
|
if err != nil {
|
|
t.Fatalf("GetMessage: %v", err)
|
|
}
|
|
if msg != nil {
|
|
t.Error("expected nil for non-existent message")
|
|
}
|
|
}
|
|
|
|
func TestGetMessage_Fields(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "carol")
|
|
chID := seedChannel(t, database, "ch")
|
|
|
|
id, _ := database.CreateMessage(context.Background(), chID, userID, "hello world", nil)
|
|
|
|
msg, err := database.GetMessage(context.Background(), id)
|
|
if err != nil {
|
|
t.Fatalf("GetMessage: %v", err)
|
|
}
|
|
if msg == nil {
|
|
t.Fatal("expected message, got nil")
|
|
}
|
|
if msg.ChannelID != chID {
|
|
t.Errorf("ChannelID = %d, want %d", msg.ChannelID, chID)
|
|
}
|
|
if msg.UserID != userID {
|
|
t.Errorf("UserID = %d, want %d", msg.UserID, userID)
|
|
}
|
|
if msg.Deleted {
|
|
t.Error("expected Deleted=false for new message")
|
|
}
|
|
if msg.Pinned {
|
|
t.Error("expected Pinned=false for new message")
|
|
}
|
|
if msg.EditedAt != nil {
|
|
t.Error("expected EditedAt=nil for new message")
|
|
}
|
|
}
|
|
|
|
// ─── GetMessages ──────────────────────────────────────────────────────────────
|
|
|
|
func TestGetMessages_EmptyChannel(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
chID := seedChannel(t, database, "empty")
|
|
|
|
msgs, err := database.GetMessages(context.Background(), chID, 0, 50)
|
|
if err != nil {
|
|
t.Fatalf("GetMessages: %v", err)
|
|
}
|
|
if len(msgs) != 0 {
|
|
t.Errorf("expected 0 messages, got %d", len(msgs))
|
|
}
|
|
}
|
|
|
|
func TestGetMessages_ReturnsMessages(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "dave")
|
|
chID := seedChannel(t, database, "ch")
|
|
|
|
for i := range 3 {
|
|
_, err := database.CreateMessage(context.Background(), chID, userID, "msg", nil)
|
|
if err != nil {
|
|
t.Fatalf("CreateMessage %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
msgs, err := database.GetMessages(context.Background(), chID, 0, 50)
|
|
if err != nil {
|
|
t.Fatalf("GetMessages: %v", err)
|
|
}
|
|
if len(msgs) != 3 {
|
|
t.Errorf("expected 3 messages, got %d", len(msgs))
|
|
}
|
|
}
|
|
|
|
func TestGetMessages_LimitRespected(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "eve")
|
|
chID := seedChannel(t, database, "ch")
|
|
|
|
for range 10 {
|
|
_, _ = database.CreateMessage(context.Background(), chID, userID, "msg", nil)
|
|
}
|
|
|
|
msgs, _ := database.GetMessages(context.Background(), chID, 0, 5)
|
|
if len(msgs) != 5 {
|
|
t.Errorf("expected 5 messages (limit), got %d", len(msgs))
|
|
}
|
|
}
|
|
|
|
func TestGetMessages_BeforePagination(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "frank")
|
|
chID := seedChannel(t, database, "ch")
|
|
|
|
ids := make([]int64, 0, 5)
|
|
for range 5 {
|
|
id, _ := database.CreateMessage(context.Background(), chID, userID, "msg", nil)
|
|
ids = append(ids, id)
|
|
}
|
|
|
|
// Get messages before the 4th message (should get 3 messages: ids 0,1,2).
|
|
msgs, _ := database.GetMessages(context.Background(), chID, ids[3], 50)
|
|
if len(msgs) != 3 {
|
|
t.Errorf("expected 3 messages before id %d, got %d", ids[3], len(msgs))
|
|
}
|
|
}
|
|
|
|
func TestGetMessages_IncludesUsername(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "grace")
|
|
chID := seedChannel(t, database, "ch")
|
|
|
|
_, _ = database.CreateMessage(context.Background(), chID, userID, "hi", nil)
|
|
msgs, _ := database.GetMessages(context.Background(), chID, 0, 50)
|
|
|
|
if len(msgs) == 0 {
|
|
t.Fatal("expected messages")
|
|
}
|
|
if msgs[0].Username != "grace" {
|
|
t.Errorf("Username = %q, want 'grace'", msgs[0].Username)
|
|
}
|
|
}
|
|
|
|
// ─── EditMessage ──────────────────────────────────────────────────────────────
|
|
|
|
func TestEditMessage_OwnerCanEdit(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "henry")
|
|
chID := seedChannel(t, database, "ch")
|
|
|
|
id, _ := database.CreateMessage(context.Background(), chID, userID, "original", nil)
|
|
|
|
if err := database.EditMessage(context.Background(), id, userID, "updated"); err != nil {
|
|
t.Fatalf("EditMessage: %v", err)
|
|
}
|
|
|
|
msg, _ := database.GetMessage(context.Background(), id)
|
|
if msg.Content != "updated" {
|
|
t.Errorf("Content = %q, want 'updated'", msg.Content)
|
|
}
|
|
if msg.EditedAt == nil {
|
|
t.Error("EditedAt should be set after edit")
|
|
}
|
|
}
|
|
|
|
func TestEditMessage_NonOwnerCannotEdit(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
ownerID := seedUser(t, database, "ivan")
|
|
otherID := seedUser(t, database, "julia")
|
|
chID := seedChannel(t, database, "ch")
|
|
|
|
id, _ := database.CreateMessage(context.Background(), chID, ownerID, "original", nil)
|
|
|
|
err := database.EditMessage(context.Background(), id, otherID, "hacked")
|
|
if err == nil {
|
|
t.Error("EditMessage by non-owner should return error")
|
|
}
|
|
}
|
|
|
|
func TestEditMessage_NotFound(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "kim")
|
|
|
|
err := database.EditMessage(context.Background(), 9999, userID, "x")
|
|
if err == nil {
|
|
t.Error("EditMessage non-existent should return error")
|
|
}
|
|
}
|
|
|
|
// ─── DeleteMessage ────────────────────────────────────────────────────────────
|
|
|
|
func TestDeleteMessage_OwnerCanDelete(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "larry")
|
|
chID := seedChannel(t, database, "ch")
|
|
|
|
id, _ := database.CreateMessage(context.Background(), chID, userID, "bye", nil)
|
|
|
|
if err := database.DeleteMessage(context.Background(), id, userID, false); err != nil {
|
|
t.Fatalf("DeleteMessage: %v", err)
|
|
}
|
|
|
|
msg, _ := database.GetMessage(context.Background(), id)
|
|
if msg == nil {
|
|
t.Fatal("soft-deleted message should still exist in DB")
|
|
}
|
|
if !msg.Deleted {
|
|
t.Error("expected Deleted=true after soft delete")
|
|
}
|
|
}
|
|
|
|
func TestDeleteMessage_ContentPreservedAfterSoftDelete(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "mia")
|
|
chID := seedChannel(t, database, "ch")
|
|
|
|
id, _ := database.CreateMessage(context.Background(), chID, userID, "sensitive", nil)
|
|
_ = database.DeleteMessage(context.Background(), id, userID, false)
|
|
|
|
msg, _ := database.GetMessage(context.Background(), id)
|
|
// Content preserved for broadcast (soft delete only flags deleted=1).
|
|
if msg.Content == "" {
|
|
t.Error("content should be preserved on soft delete for broadcast purposes")
|
|
}
|
|
}
|
|
|
|
func TestDeleteMessage_NonOwnerBlockedWithoutMod(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
ownerID := seedUser(t, database, "nate")
|
|
otherID := seedUser(t, database, "olivia")
|
|
chID := seedChannel(t, database, "ch")
|
|
|
|
id, _ := database.CreateMessage(context.Background(), chID, ownerID, "msg", nil)
|
|
|
|
err := database.DeleteMessage(context.Background(), id, otherID, false)
|
|
if err == nil {
|
|
t.Error("DeleteMessage by non-owner non-mod should return error")
|
|
}
|
|
}
|
|
|
|
func TestDeleteMessage_ModCanDeleteAny(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
ownerID := seedUser(t, database, "pete")
|
|
modID := seedUser(t, database, "quinn")
|
|
chID := seedChannel(t, database, "ch")
|
|
|
|
id, _ := database.CreateMessage(context.Background(), chID, ownerID, "msg", nil)
|
|
|
|
if err := database.DeleteMessage(context.Background(), id, modID, true); err != nil {
|
|
t.Fatalf("DeleteMessage by mod: %v", err)
|
|
}
|
|
|
|
msg, _ := database.GetMessage(context.Background(), id)
|
|
if !msg.Deleted {
|
|
t.Error("expected Deleted=true after mod delete")
|
|
}
|
|
}
|
|
|
|
func TestDeleteMessage_NotFound(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "rachel")
|
|
|
|
err := database.DeleteMessage(context.Background(), 9999, userID, true)
|
|
if err == nil {
|
|
t.Error("DeleteMessage non-existent should return error")
|
|
}
|
|
}
|
|
|
|
// ─── Reactions ────────────────────────────────────────────────────────────────
|
|
|
|
func TestAddReaction_Success(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "sam")
|
|
chID := seedChannel(t, database, "ch")
|
|
msgID, _ := database.CreateMessage(context.Background(), chID, userID, "hi", nil)
|
|
|
|
if err := database.AddReaction(context.Background(), msgID, userID, "👍"); err != nil {
|
|
t.Fatalf("AddReaction: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAddReaction_UniqueConstraint(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "tina")
|
|
chID := seedChannel(t, database, "ch")
|
|
msgID, _ := database.CreateMessage(context.Background(), chID, userID, "hi", nil)
|
|
|
|
_ = database.AddReaction(context.Background(), msgID, userID, "❤️")
|
|
err := database.AddReaction(context.Background(), msgID, userID, "❤️")
|
|
if err == nil {
|
|
t.Error("adding duplicate reaction should return error")
|
|
}
|
|
}
|
|
|
|
func TestRemoveReaction_Success(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "uma")
|
|
chID := seedChannel(t, database, "ch")
|
|
msgID, _ := database.CreateMessage(context.Background(), chID, userID, "hi", nil)
|
|
|
|
_ = database.AddReaction(context.Background(), msgID, userID, "😂")
|
|
if err := database.RemoveReaction(context.Background(), msgID, userID, "😂"); err != nil {
|
|
t.Fatalf("RemoveReaction: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRemoveReaction_NotFound(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "victor")
|
|
chID := seedChannel(t, database, "ch")
|
|
msgID, _ := database.CreateMessage(context.Background(), chID, userID, "hi", nil)
|
|
|
|
err := database.RemoveReaction(context.Background(), msgID, userID, "🔥")
|
|
if err == nil {
|
|
t.Error("removing non-existent reaction should return error")
|
|
}
|
|
}
|
|
|
|
func TestGetReactions_Empty(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "wendy")
|
|
chID := seedChannel(t, database, "ch")
|
|
msgID, _ := database.CreateMessage(context.Background(), chID, userID, "hi", nil)
|
|
|
|
counts, err := database.GetReactions(context.Background(), msgID)
|
|
if err != nil {
|
|
t.Fatalf("GetReactions: %v", err)
|
|
}
|
|
if len(counts) != 0 {
|
|
t.Errorf("expected 0 reactions, got %d", len(counts))
|
|
}
|
|
}
|
|
|
|
func TestGetReactions_Counts(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
u1 := seedUser(t, database, "xavier")
|
|
u2 := seedUser(t, database, "yvonne")
|
|
chID := seedChannel(t, database, "ch")
|
|
msgID, _ := database.CreateMessage(context.Background(), chID, u1, "hi", nil)
|
|
|
|
_ = database.AddReaction(context.Background(), msgID, u1, "👍")
|
|
_ = database.AddReaction(context.Background(), msgID, u2, "👍")
|
|
_ = database.AddReaction(context.Background(), msgID, u1, "❤️")
|
|
|
|
counts, _ := database.GetReactions(context.Background(), msgID)
|
|
if len(counts) != 2 {
|
|
t.Fatalf("expected 2 emoji types, got %d", len(counts))
|
|
}
|
|
for _, rc := range counts {
|
|
switch rc.Emoji {
|
|
case "👍":
|
|
if rc.Count != 2 {
|
|
t.Errorf("👍 count = %d, want 2", rc.Count)
|
|
}
|
|
case "❤️":
|
|
if rc.Count != 1 {
|
|
t.Errorf("❤️ count = %d, want 1", rc.Count)
|
|
}
|
|
default:
|
|
t.Errorf("unexpected emoji %q", rc.Emoji)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── SearchMessages ───────────────────────────────────────────────────────────
|
|
|
|
func TestSearchMessages_FindsMatch(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "zara")
|
|
chID := seedChannel(t, database, "searchch")
|
|
|
|
_, _ = database.CreateMessage(context.Background(), chID, userID, "hello world fts test", nil)
|
|
_, _ = database.CreateMessage(context.Background(), chID, userID, "unrelated content here", nil)
|
|
|
|
results, err := database.SearchMessages(context.Background(), "hello", nil, 10)
|
|
if err != nil {
|
|
t.Fatalf("SearchMessages: %v", err)
|
|
}
|
|
if len(results) != 1 {
|
|
t.Errorf("expected 1 result, got %d", len(results))
|
|
}
|
|
if results[0].Content != "hello world fts test" {
|
|
t.Errorf("Content = %q, want 'hello world fts test'", results[0].Content)
|
|
}
|
|
}
|
|
|
|
func TestSearchMessages_FilterByChannel(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "adam")
|
|
ch1 := seedChannel(t, database, "ch1")
|
|
ch2 := seedChannel(t, database, "ch2")
|
|
|
|
_, _ = database.CreateMessage(context.Background(), ch1, userID, "needle in channel 1", nil)
|
|
_, _ = database.CreateMessage(context.Background(), ch2, userID, "needle in channel 2", nil)
|
|
|
|
results, _ := database.SearchMessages(context.Background(), "needle", &ch1, 10)
|
|
if len(results) != 1 {
|
|
t.Errorf("expected 1 result in ch1, got %d", len(results))
|
|
}
|
|
if results[0].ChannelID != ch1 {
|
|
t.Errorf("ChannelID = %d, want %d", results[0].ChannelID, ch1)
|
|
}
|
|
}
|
|
|
|
func TestSearchMessages_NoResults(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "beth")
|
|
chID := seedChannel(t, database, "ch")
|
|
_, _ = database.CreateMessage(context.Background(), chID, userID, "hello there", nil)
|
|
|
|
results, _ := database.SearchMessages(context.Background(), "xyzzy", nil, 10)
|
|
if len(results) != 0 {
|
|
t.Errorf("expected 0 results, got %d", len(results))
|
|
}
|
|
}
|
|
|
|
func TestSearchMessages_LimitRespected(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "carl")
|
|
chID := seedChannel(t, database, "ch")
|
|
|
|
for range 5 {
|
|
_, _ = database.CreateMessage(context.Background(), chID, userID, "searchable keyword content", nil)
|
|
}
|
|
|
|
results, _ := database.SearchMessages(context.Background(), "keyword", nil, 3)
|
|
if len(results) != 3 {
|
|
t.Errorf("expected 3 results (limit), got %d", len(results))
|
|
}
|
|
}
|
|
|
|
func TestSearchMessages_DeletedNotReturned(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "diana")
|
|
chID := seedChannel(t, database, "ch")
|
|
|
|
id, _ := database.CreateMessage(context.Background(), chID, userID, "vanishing keyword message", nil)
|
|
_ = database.DeleteMessage(context.Background(), id, userID, false)
|
|
|
|
results, _ := database.SearchMessages(context.Background(), "vanishing", nil, 10)
|
|
if len(results) != 0 {
|
|
t.Errorf("expected 0 results (deleted excluded), got %d", len(results))
|
|
}
|
|
}
|
|
|
|
// ─── UpdateReadState ──────────────────────────────────────────────────────────
|
|
|
|
func TestUpdateReadState_Upsert(t *testing.T) {
|
|
database := openMigratedMemory(t)
|
|
userID := seedUser(t, database, "ella")
|
|
chID := seedChannel(t, database, "ch")
|
|
msgID, _ := database.CreateMessage(context.Background(), chID, userID, "msg", nil)
|
|
|
|
if err := database.UpdateReadState(context.Background(), userID, chID, msgID); err != nil {
|
|
t.Fatalf("UpdateReadState: %v", err)
|
|
}
|
|
|
|
// Update again with higher message ID — should not error.
|
|
msgID2, _ := database.CreateMessage(context.Background(), chID, userID, "msg2", nil)
|
|
if err := database.UpdateReadState(context.Background(), userID, chID, msgID2); err != nil {
|
|
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(context.Background(), 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(context.Background(), chID, userID, "hello api", nil)
|
|
|
|
msgs, err := database.GetMessagesForAPI(context.Background(), 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")
|
|
|
|
ids := make([]int64, 0, 5)
|
|
for range 5 {
|
|
id, _ := database.CreateMessage(context.Background(), chID, userID, "msg", nil)
|
|
ids = append(ids, id)
|
|
}
|
|
|
|
msgs, err := database.GetMessagesForAPI(context.Background(), 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(context.Background(), chID, u1, "react me", nil)
|
|
_ = database.AddReaction(context.Background(), msgID, u1, "👍")
|
|
_ = database.AddReaction(context.Background(), msgID, u2, "👍")
|
|
|
|
msgs, err := database.GetMessagesForAPI(context.Background(), 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(context.Background(), chID, userID, "deleted msg", nil)
|
|
_ = database.DeleteMessage(context.Background(), id, userID, false)
|
|
_, _ = database.CreateMessage(context.Background(), chID, userID, "visible msg", nil)
|
|
|
|
msgs, err := database.GetMessagesForAPI(context.Background(), 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(context.Background(), 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(context.Background(), chID, userID, "msg1", nil)
|
|
_, _ = database.CreateMessage(context.Background(), chID, userID, "msg2", nil)
|
|
_, _ = database.CreateMessage(context.Background(), chID, userID, "msg3", nil)
|
|
|
|
_ = database.UpdateReadState(context.Background(), userID, chID, msg1)
|
|
|
|
counts, err := database.GetChannelUnreadCounts(context.Background(), 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(context.Background(), 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(context.Background(), chID, userID, "first", nil)
|
|
_, _ = database.CreateMessage(context.Background(), chID, userID, "second", nil)
|
|
lastID, _ := database.CreateMessage(context.Background(), chID, userID, "third", nil)
|
|
|
|
id, err := database.GetLatestMessageID(context.Background(), 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(context.Background(), chID, userID, "keep", nil)
|
|
id2, _ := database.CreateMessage(context.Background(), chID, userID, "delete me", nil)
|
|
_ = database.DeleteMessage(context.Background(), id2, userID, false)
|
|
|
|
latestID, err := database.GetLatestMessageID(context.Background(), chID)
|
|
if err != nil {
|
|
t.Fatalf("GetLatestMessageID: %v", err)
|
|
}
|
|
if latestID != id1 {
|
|
t.Errorf("GetLatestMessageID = %d, want %d (deleted excluded)", latestID, id1)
|
|
}
|
|
}
|