2026-09-27 19:42:49 +03:00
|
|
|
package store
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"os"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// TestIntegration exercises basic CRUD operations against a real database.
|
|
|
|
|
func TestIntegration(t *testing.T) {
|
|
|
|
|
url := os.Getenv("MESSENGER_TEST_DATABASE_URL")
|
|
|
|
|
if url == "" {
|
|
|
|
|
t.Skip("skipping integration test; MESSENGER_TEST_DATABASE_URL not set")
|
|
|
|
|
}
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
pool, err := pgxpool.New(ctx, url)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("unable to connect to db: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer pool.Close()
|
|
|
|
|
|
|
|
|
|
db := New(pool)
|
|
|
|
|
if err := db.Migrate(ctx); err != nil {
|
|
|
|
|
t.Fatalf("Migrate failed: %v", err)
|
|
|
|
|
}
|
2026-09-27 20:15:38 +03:00
|
|
|
if _, err := pool.Exec(ctx, "TRUNCATE users, conversations, messages, contacts, sessions RESTART IDENTITY CASCADE"); err != nil {
|
|
|
|
|
t.Fatalf("truncate failed: %v", err)
|
|
|
|
|
}
|
2026-09-27 19:42:49 +03:00
|
|
|
|
|
|
|
|
// Users
|
|
|
|
|
u1, err := db.CreateUser(ctx, "user1", "User One", "hash1")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateUser u1: %v", err)
|
|
|
|
|
}
|
|
|
|
|
u2, err := db.CreateUser(ctx, "user2", "User Two", "hash2")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateUser u2: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := db.GetUserByUsername(ctx, u1.Username); err != nil {
|
|
|
|
|
t.Fatalf("GetUserByUsername: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := db.GetUserByID(ctx, u2.ID); err != nil {
|
|
|
|
|
t.Fatalf("GetUserByID: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sessions
|
|
|
|
|
exp := time.Now().Add(time.Hour)
|
|
|
|
|
sess, err := db.CreateSession(ctx, u1.ID, "rh1", "ua", "127.0.0.1", exp)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateSession: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := db.GetSessionByRefreshHash(ctx, sess.RefreshHash); err != nil {
|
|
|
|
|
t.Fatalf("GetSessionByRefreshHash: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := db.RevokeSession(ctx, sess.ID); err != nil {
|
|
|
|
|
t.Fatalf("RevokeSession: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := db.RevokeAllSessions(ctx, u1.ID); err != nil {
|
|
|
|
|
t.Fatalf("RevokeAllSessions: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Contacts
|
|
|
|
|
if err := db.AddContact(ctx, u1.ID, u2.ID); err != nil {
|
|
|
|
|
t.Fatalf("AddContact: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if ok, err := db.IsContact(ctx, u1.ID, u2.ID); err != nil || !ok {
|
|
|
|
|
t.Fatalf("IsContact: %v, %v", ok, err)
|
|
|
|
|
}
|
|
|
|
|
list, err := db.ListContacts(ctx, u1.ID)
|
|
|
|
|
if err != nil || len(list) != 1 {
|
|
|
|
|
t.Fatalf("ListContacts: got %v, err %v", list, err)
|
|
|
|
|
}
|
|
|
|
|
if err := db.RemoveContact(ctx, u1.ID, u2.ID); err != nil {
|
|
|
|
|
t.Fatalf("RemoveContact: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Conversations and messages
|
|
|
|
|
conv, err := db.CreateDM(ctx, u1.ID, u2.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateDM: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if conv2, err := db.CreateDM(ctx, u1.ID, u2.ID); err != nil || conv2.ID != conv.ID {
|
|
|
|
|
t.Fatalf("CreateDM idempotent: %v, %v", err, conv2)
|
|
|
|
|
}
|
|
|
|
|
if ok, err := db.IsMember(ctx, conv.ID, u1.ID); err != nil || !ok {
|
|
|
|
|
t.Fatalf("IsMember: %v, %v", ok, err)
|
|
|
|
|
}
|
|
|
|
|
ids, err := db.ConversationIDsForUser(ctx, u1.ID)
|
|
|
|
|
if err != nil || len(ids) == 0 {
|
|
|
|
|
t.Fatalf("ConversationIDsForUser: %v, %v", ids, err)
|
|
|
|
|
}
|
|
|
|
|
sums, err := db.ListConversationsForUser(ctx, u1.ID)
|
|
|
|
|
if err != nil || len(sums) != 1 {
|
|
|
|
|
t.Fatalf("ListConversationsForUser: %v, %v", sums, err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-27 21:59:48 +03:00
|
|
|
msg, err := db.InsertMessage(ctx, conv.ID, u1.ID, "hello", nil)
|
2026-09-27 19:42:49 +03:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("InsertMessage: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := db.GetMessageByID(ctx, msg.ID); err != nil {
|
|
|
|
|
t.Fatalf("GetMessageByID: %v", err)
|
|
|
|
|
}
|
|
|
|
|
msgs, err := db.ListMessages(ctx, conv.ID, 0, 10)
|
|
|
|
|
if err != nil || len(msgs) != 1 {
|
|
|
|
|
t.Fatalf("ListMessages: %v, %v", msgs, err)
|
|
|
|
|
}
|
|
|
|
|
if err := db.UpdateLastRead(ctx, conv.ID, u2.ID); err != nil {
|
|
|
|
|
t.Fatalf("UpdateLastRead: %v", err)
|
|
|
|
|
}
|
2026-09-27 21:59:48 +03:00
|
|
|
|
|
|
|
|
// Groups
|
|
|
|
|
u3, err := db.CreateUser(ctx, "user3", "User Three", "hash3")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateUser u3: %v", err)
|
|
|
|
|
}
|
|
|
|
|
group, err := db.CreateGroup(ctx, u1.ID, "Family", []int64{u2.ID, u1.ID})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CreateGroup: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if group.Type != "group" || group.Name != "Family" {
|
|
|
|
|
t.Fatalf("CreateGroup result = %+v", group)
|
|
|
|
|
}
|
|
|
|
|
members, err := db.ListMembers(ctx, group.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ListMembers: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(members) != 2 {
|
|
|
|
|
t.Fatalf("ListMembers len = %d, want 2", len(members))
|
|
|
|
|
}
|
|
|
|
|
role, err := db.MemberRole(ctx, group.ID, u1.ID)
|
|
|
|
|
if err != nil || role != "owner" {
|
|
|
|
|
t.Fatalf("MemberRole u1 = %q, %v; want owner", role, err)
|
|
|
|
|
}
|
|
|
|
|
if err := db.AddMember(ctx, group.ID, u3.ID); err != nil {
|
|
|
|
|
t.Fatalf("AddMember: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := db.AddMember(ctx, group.ID, u3.ID); err != nil {
|
|
|
|
|
t.Fatalf("AddMember idempotent: %v", err)
|
|
|
|
|
}
|
|
|
|
|
members, _ = db.ListMembers(ctx, group.ID)
|
|
|
|
|
if len(members) != 3 {
|
|
|
|
|
t.Fatalf("ListMembers after add = %d, want 3", len(members))
|
|
|
|
|
}
|
|
|
|
|
if err := db.RemoveMember(ctx, group.ID, u3.ID); err != nil {
|
|
|
|
|
t.Fatalf("RemoveMember: %v", err)
|
|
|
|
|
}
|
|
|
|
|
members, _ = db.ListMembers(ctx, group.ID)
|
|
|
|
|
if len(members) != 2 {
|
|
|
|
|
t.Fatalf("ListMembers after remove = %d, want 2", len(members))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reactions
|
|
|
|
|
if err := db.AddReaction(ctx, msg.ID, u2.ID, "👍"); err != nil {
|
|
|
|
|
t.Fatalf("AddReaction: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := db.AddReaction(ctx, msg.ID, u2.ID, "👍"); err != nil {
|
|
|
|
|
t.Fatalf("AddReaction idempotent: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := db.AddReaction(ctx, msg.ID, u1.ID, "❤️"); err != nil {
|
|
|
|
|
t.Fatalf("AddReaction u1: %v", err)
|
|
|
|
|
}
|
|
|
|
|
reactions, err := db.ReactionsForMessages(ctx, []int64{msg.ID})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ReactionsForMessages: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(reactions) != 2 {
|
|
|
|
|
t.Fatalf("ReactionsForMessages len = %d, want 2", len(reactions))
|
|
|
|
|
}
|
|
|
|
|
if err := db.RemoveReaction(ctx, msg.ID, u2.ID, "👍"); err != nil {
|
|
|
|
|
t.Fatalf("RemoveReaction: %v", err)
|
|
|
|
|
}
|
|
|
|
|
reactions, _ = db.ReactionsForMessages(ctx, []int64{msg.ID})
|
|
|
|
|
if len(reactions) != 1 {
|
|
|
|
|
t.Fatalf("ReactionsForMessages after remove = %d, want 1", len(reactions))
|
|
|
|
|
}
|
|
|
|
|
belongs, err := db.MessageBelongsToConversation(ctx, conv.ID, msg.ID)
|
|
|
|
|
if err != nil || !belongs {
|
|
|
|
|
t.Fatalf("MessageBelongsToConversation = %v, %v; want true", belongs, err)
|
|
|
|
|
}
|
2026-09-27 19:42:49 +03:00
|
|
|
}
|