package db_test import ( "context" "fmt" "path/filepath" "testing" "time" "git.eeqj.de/sneak/chat/internal/db" ) const ( nickAlice = "alice" nickBob = "bob" nickCharlie = "charlie" ) // setupTestDB creates a fresh database in a temp directory with // all migrations applied. func setupTestDB(t *testing.T) *db.Database { t.Helper() dir := t.TempDir() dsn := fmt.Sprintf( "file:%s?_journal_mode=WAL", filepath.Join(dir, "test.db"), ) d, err := db.NewTest(dsn) if err != nil { t.Fatalf("failed to create test database: %v", err) } t.Cleanup(func() { _ = d.GetDB().Close() }) return d } func TestCreateUser(t *testing.T) { t.Parallel() d := setupTestDB(t) ctx := context.Background() u, err := d.CreateUserModel(ctx, "u1", nickAlice, "hash1") if err != nil { t.Fatalf("CreateUser: %v", err) } if u.ID != "u1" || u.Nick != nickAlice { t.Errorf("got user %+v", u) } } func TestCreateAuthToken(t *testing.T) { t.Parallel() d := setupTestDB(t) ctx := context.Background() _, err := d.CreateUserModel(ctx, "u1", nickAlice, "h") if err != nil { t.Fatalf("CreateUser: %v", err) } tok, err := d.CreateAuthToken(ctx, "tok1", "u1") if err != nil { t.Fatalf("CreateAuthToken: %v", err) } if tok.Token != "tok1" || tok.UserID != "u1" { t.Errorf("unexpected token: %+v", tok) } u, err := tok.User(ctx) if err != nil { t.Fatalf("AuthToken.User: %v", err) } if u.ID != "u1" || u.Nick != nickAlice { t.Errorf("AuthToken.User got %+v", u) } } func TestCreateChannel(t *testing.T) { t.Parallel() d := setupTestDB(t) ctx := context.Background() ch, err := d.CreateChannel( ctx, "c1", "#general", "welcome", "+n", ) if err != nil { t.Fatalf("CreateChannel: %v", err) } if ch.ID != "c1" || ch.Name != "#general" { t.Errorf("unexpected channel: %+v", ch) } } func TestAddChannelMember(t *testing.T) { t.Parallel() d := setupTestDB(t) ctx := context.Background() _, _ = d.CreateUserModel(ctx, "u1", nickAlice, "h") _, _ = d.CreateChannel(ctx, "c1", "#general", "", "") cm, err := d.AddChannelMember(ctx, "c1", "u1", "+o") if err != nil { t.Fatalf("AddChannelMember: %v", err) } if cm.ChannelID != "c1" || cm.Modes != "+o" { t.Errorf("unexpected member: %+v", cm) } } func TestCreateMessage(t *testing.T) { t.Parallel() d := setupTestDB(t) ctx := context.Background() _, _ = d.CreateUserModel(ctx, "u1", nickAlice, "h") msg, err := d.CreateMessage( ctx, "m1", "u1", nickAlice, "#general", "message", "hello", ) if err != nil { t.Fatalf("CreateMessage: %v", err) } if msg.ID != "m1" || msg.Body != "hello" { t.Errorf("unexpected message: %+v", msg) } } func TestQueueMessage(t *testing.T) { t.Parallel() d := setupTestDB(t) ctx := context.Background() _, _ = d.CreateUserModel(ctx, "u1", nickAlice, "h") _, _ = d.CreateUserModel(ctx, "u2", nickBob, "h") _, _ = d.CreateMessage( ctx, "m1", "u1", nickAlice, "u2", "message", "hi", ) mq, err := d.QueueMessage(ctx, "u2", "m1") if err != nil { t.Fatalf("QueueMessage: %v", err) } if mq.UserID != "u2" || mq.MessageID != "m1" { t.Errorf("unexpected queue entry: %+v", mq) } } func TestCreateSession(t *testing.T) { t.Parallel() d := setupTestDB(t) ctx := context.Background() _, _ = d.CreateUserModel(ctx, "u1", nickAlice, "h") sess, err := d.CreateSession(ctx, "s1", "u1") if err != nil { t.Fatalf("CreateSession: %v", err) } if sess.ID != "s1" || sess.UserID != "u1" { t.Errorf("unexpected session: %+v", sess) } u, err := sess.User(ctx) if err != nil { t.Fatalf("Session.User: %v", err) } if u.ID != "u1" { t.Errorf("Session.User got %v, want u1", u.ID) } } func TestCreateServerLink(t *testing.T) { t.Parallel() d := setupTestDB(t) ctx := context.Background() sl, err := d.CreateServerLink( ctx, "sl1", "peer1", "https://peer.example.com", "keyhash", true, ) if err != nil { t.Fatalf("CreateServerLink: %v", err) } if sl.ID != "sl1" || !sl.IsActive { t.Errorf("unexpected server link: %+v", sl) } } func TestUserChannels(t *testing.T) { t.Parallel() d := setupTestDB(t) ctx := context.Background() u, _ := d.CreateUserModel(ctx, "u1", nickAlice, "h") _, _ = d.CreateChannel(ctx, "c1", "#alpha", "", "") _, _ = d.CreateChannel(ctx, "c2", "#beta", "", "") _, _ = d.AddChannelMember(ctx, "c1", "u1", "") _, _ = d.AddChannelMember(ctx, "c2", "u1", "") channels, err := u.Channels(ctx) if err != nil { t.Fatalf("User.Channels: %v", err) } if len(channels) != 2 { t.Fatalf("expected 2 channels, got %d", len(channels)) } if channels[0].Name != "#alpha" { t.Errorf("first channel: got %s", channels[0].Name) } if channels[1].Name != "#beta" { t.Errorf("second channel: got %s", channels[1].Name) } } func TestUserChannelsEmpty(t *testing.T) { t.Parallel() d := setupTestDB(t) ctx := context.Background() u, _ := d.CreateUserModel(ctx, "u1", nickAlice, "h") channels, err := u.Channels(ctx) if err != nil { t.Fatalf("User.Channels: %v", err) } if len(channels) != 0 { t.Errorf("expected 0 channels, got %d", len(channels)) } } func TestUserQueuedMessages(t *testing.T) { t.Parallel() d := setupTestDB(t) ctx := context.Background() u, _ := d.CreateUserModel(ctx, "u1", nickAlice, "h") _, _ = d.CreateUserModel(ctx, "u2", nickBob, "h") for i := range 3 { id := fmt.Sprintf("m%d", i) _, _ = d.CreateMessage( ctx, id, "u2", nickBob, "u1", "message", fmt.Sprintf("msg%d", i), ) time.Sleep(10 * time.Millisecond) _, _ = d.QueueMessage(ctx, "u1", id) } msgs, err := u.QueuedMessages(ctx) if err != nil { t.Fatalf("User.QueuedMessages: %v", err) } if len(msgs) != 3 { t.Fatalf("expected 3 messages, got %d", len(msgs)) } for i, msg := range msgs { want := fmt.Sprintf("msg%d", i) if msg.Body != want { t.Errorf("msg %d: got %q, want %q", i, msg.Body, want) } } } func TestUserQueuedMessagesEmpty(t *testing.T) { t.Parallel() d := setupTestDB(t) ctx := context.Background() u, _ := d.CreateUserModel(ctx, "u1", nickAlice, "h") msgs, err := u.QueuedMessages(ctx) if err != nil { t.Fatalf("User.QueuedMessages: %v", err) } if len(msgs) != 0 { t.Errorf("expected 0 messages, got %d", len(msgs)) } } func TestChannelMembers(t *testing.T) { t.Parallel() d := setupTestDB(t) ctx := context.Background() ch, _ := d.CreateChannel(ctx, "c1", "#general", "", "") _, _ = d.CreateUserModel(ctx, "u1", nickAlice, "h") _, _ = d.CreateUserModel(ctx, "u2", nickBob, "h") _, _ = d.CreateUserModel(ctx, "u3", nickCharlie, "h") _, _ = d.AddChannelMember(ctx, "c1", "u1", "+o") _, _ = d.AddChannelMember(ctx, "c1", "u2", "+v") _, _ = d.AddChannelMember(ctx, "c1", "u3", "") members, err := ch.Members(ctx) if err != nil { t.Fatalf("Channel.Members: %v", err) } if len(members) != 3 { t.Fatalf("expected 3 members, got %d", len(members)) } nicks := map[string]bool{} for _, m := range members { nicks[m.Nick] = true } for _, want := range []string{ nickAlice, nickBob, nickCharlie, } { if !nicks[want] { t.Errorf("missing nick %q", want) } } } func TestChannelMembersEmpty(t *testing.T) { t.Parallel() d := setupTestDB(t) ctx := context.Background() ch, _ := d.CreateChannel(ctx, "c1", "#empty", "", "") members, err := ch.Members(ctx) if err != nil { t.Fatalf("Channel.Members: %v", err) } if len(members) != 0 { t.Errorf("expected 0, got %d", len(members)) } } func TestChannelRecentMessages(t *testing.T) { t.Parallel() d := setupTestDB(t) ctx := context.Background() ch, _ := d.CreateChannel(ctx, "c1", "#general", "", "") _, _ = d.CreateUserModel(ctx, "u1", nickAlice, "h") for i := range 5 { id := fmt.Sprintf("m%d", i) _, _ = d.CreateMessage( ctx, id, "u1", nickAlice, "#general", "message", fmt.Sprintf("msg%d", i), ) time.Sleep(10 * time.Millisecond) } msgs, err := ch.RecentMessages(ctx, 3) if err != nil { t.Fatalf("RecentMessages: %v", err) } if len(msgs) != 3 { t.Fatalf("expected 3, got %d", len(msgs)) } if msgs[0].Body != "msg4" { t.Errorf("first: got %q, want msg4", msgs[0].Body) } if msgs[2].Body != "msg2" { t.Errorf("last: got %q, want msg2", msgs[2].Body) } } func TestChannelRecentMessagesLargeLimit(t *testing.T) { t.Parallel() d := setupTestDB(t) ctx := context.Background() ch, _ := d.CreateChannel(ctx, "c1", "#general", "", "") _, _ = d.CreateUserModel(ctx, "u1", nickAlice, "h") _, _ = d.CreateMessage( ctx, "m1", "u1", nickAlice, "#general", "message", "only", ) msgs, err := ch.RecentMessages(ctx, 100) if err != nil { t.Fatalf("RecentMessages: %v", err) } if len(msgs) != 1 { t.Errorf("expected 1, got %d", len(msgs)) } } func TestChannelMemberUser(t *testing.T) { t.Parallel() d := setupTestDB(t) ctx := context.Background() _, _ = d.CreateUserModel(ctx, "u1", nickAlice, "h") _, _ = d.CreateChannel(ctx, "c1", "#general", "", "") cm, _ := d.AddChannelMember(ctx, "c1", "u1", "+o") u, err := cm.User(ctx) if err != nil { t.Fatalf("ChannelMember.User: %v", err) } if u.ID != "u1" || u.Nick != nickAlice { t.Errorf("got %+v", u) } } func TestChannelMemberChannel(t *testing.T) { t.Parallel() d := setupTestDB(t) ctx := context.Background() _, _ = d.CreateUserModel(ctx, "u1", nickAlice, "h") _, _ = d.CreateChannel(ctx, "c1", "#general", "topic", "+n") cm, _ := d.AddChannelMember(ctx, "c1", "u1", "") ch, err := cm.Channel(ctx) if err != nil { t.Fatalf("ChannelMember.Channel: %v", err) } if ch.ID != "c1" || ch.Topic != "topic" { t.Errorf("got %+v", ch) } } func TestDMMessage(t *testing.T) { t.Parallel() d := setupTestDB(t) ctx := context.Background() _, _ = d.CreateUserModel(ctx, "u1", nickAlice, "h") _, _ = d.CreateUserModel(ctx, "u2", nickBob, "h") msg, err := d.CreateMessage( ctx, "m1", "u1", nickAlice, "u2", "message", "hey", ) if err != nil { t.Fatalf("CreateMessage DM: %v", err) } if msg.Target != "u2" { t.Errorf("target: got %q, want u2", msg.Target) } }