clickclack/apps/api/internal/store/sqlite/sqlite_test.go
2026-05-08 05:36:16 +01:00

233 lines
6.9 KiB
Go

package sqlite
import (
"bytes"
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/openclaw/clickclack/apps/api/internal/store"
)
func TestStoreValidationAndAdminHelpers(t *testing.T) {
t.Parallel()
ctx := context.Background()
st := newTestStore(t)
owner, err := st.EnsureBootstrap(ctx, "Owner", "owner@example.com")
if err != nil {
t.Fatal(err)
}
workspaces, err := st.ListWorkspaces(ctx, owner.ID)
if err != nil {
t.Fatal(err)
}
workspace := workspaces[0]
channels, err := st.ListChannels(ctx, workspace.ID, owner.ID)
if err != nil {
t.Fatal(err)
}
channel := channels[0]
if _, err := st.CreateWorkspace(ctx, store.CreateWorkspaceInput{Name: "ClickClack", Slug: workspace.Slug}, owner.ID); err == nil {
t.Fatal("expected duplicate workspace slug error")
}
if _, _, err := st.CreateMessage(ctx, store.CreateMessageInput{ChannelID: channel.ID, AuthorID: owner.ID}); err == nil {
t.Fatal("expected empty message error")
}
if _, _, _, err := st.CreateThreadReply(ctx, store.CreateThreadReplyInput{RootMessageID: channel.ID, AuthorID: owner.ID, Body: "nope"}); err == nil {
t.Fatal("expected missing root message error")
}
if results, err := st.SearchMessages(ctx, workspace.ID, owner.ID, "", 10); err != nil || len(results) != 0 {
t.Fatalf("expected empty search results, got %#v err=%v", results, err)
}
if _, err := st.CreateInvite(ctx, workspace.ID, owner.ID); err != nil {
t.Fatal(err)
}
link, err := st.CreateMagicLink(ctx, "magic@example.com", "Magic User")
if err != nil {
t.Fatal(err)
}
magicUser, session, err := st.ConsumeMagicLink(ctx, link.Token)
if err != nil {
t.Fatal(err)
}
if magicUser.DisplayName != "Magic User" || session.Token == "" {
t.Fatalf("unexpected magic auth result: %#v %#v", magicUser, session)
}
sessionUser, err := st.GetSessionUser(ctx, session.Token)
if err != nil {
t.Fatal(err)
}
if sessionUser.ID != magicUser.ID {
t.Fatalf("expected session user %s, got %s", magicUser.ID, sessionUser.ID)
}
if _, _, err := st.ConsumeMagicLink(ctx, link.Token); err == nil {
t.Fatal("expected consumed magic link error")
}
if _, err := st.CreateMagicLink(ctx, "", "No Email"); err == nil {
t.Fatal("expected missing email error")
}
var exported bytes.Buffer
if err := st.ExportJSON(ctx, &exported); err != nil {
t.Fatal(err)
}
var exportBody map[string][]map[string]any
if err := json.Unmarshal(exported.Bytes(), &exportBody); err != nil {
t.Fatal(err)
}
if len(exportBody["auth_magic_links"]) == 0 || len(exportBody["sessions"]) == 0 {
t.Fatalf("expected auth tables in export, got keys %#v", exportBody)
}
if err := st.Backup(ctx, filepath.Join(t.TempDir(), "backup.db")); err != nil {
t.Fatal(err)
}
if _, err := st.db.ExecContext(ctx, `DROP TABLE sessions`); err != nil {
t.Fatal(err)
}
if err := st.ExportJSON(ctx, &bytes.Buffer{}); err == nil {
t.Fatal("expected export failure")
}
second, err := st.CreateUser(ctx, store.CreateUserInput{DisplayName: "Second", Email: "second@example.com"})
if err != nil {
t.Fatal(err)
}
if _, err := st.CreateDirectConversation(ctx, store.CreateDirectConversationInput{WorkspaceID: workspace.ID, UserID: owner.ID, MemberIDs: []string{second.ID}}); err == nil {
t.Fatal("expected dm membership error for second user")
}
if err := st.AddWorkspaceMember(ctx, workspace.ID, second.ID, "member"); err != nil {
t.Fatal(err)
}
dm, err := st.CreateDirectConversation(ctx, store.CreateDirectConversationInput{WorkspaceID: workspace.ID, UserID: owner.ID, MemberIDs: []string{second.ID}})
if err != nil {
t.Fatal(err)
}
dms, err := st.ListDirectConversations(ctx, workspace.ID, owner.ID)
if err != nil {
t.Fatal(err)
}
if len(dms) != 1 || dms[0].ID != dm.ID {
t.Fatalf("unexpected dm list: %#v", dms)
}
if _, _, err := st.CreateDirectMessage(ctx, store.CreateDirectMessageInput{ConversationID: dm.ID, AuthorID: second.ID}); err == nil {
t.Fatal("expected empty dm message error")
}
}
func TestOpenRejectsBadDirectory(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "not-a-dir")
if err := os.WriteFile(path, []byte("file"), 0o644); err != nil {
t.Fatal(err)
}
if _, err := Open("sqlite://" + filepath.Join(path, "db.sqlite")); err == nil {
t.Fatal("expected bad directory error")
}
}
func TestStoreClosedDatabaseErrors(t *testing.T) {
t.Parallel()
ctx := context.Background()
st := newTestStore(t)
if err := st.db.Close(); err != nil {
t.Fatal(err)
}
errorCases := []struct {
name string
fn func() error
}{
{"migrate", func() error { return st.Migrate(ctx) }},
{"create user", func() error {
_, err := st.CreateUser(ctx, store.CreateUserInput{DisplayName: "x"})
return err
}},
{"first user", func() error {
_, err := st.FirstUser(ctx)
return err
}},
{"get user", func() error {
_, err := st.GetUser(ctx, "usr_missing")
return err
}},
{"list workspaces", func() error {
_, err := st.ListWorkspaces(ctx, "usr_missing")
return err
}},
{"create workspace", func() error {
_, err := st.CreateWorkspace(ctx, store.CreateWorkspaceInput{Name: "x"}, "usr_missing")
return err
}},
{"create channel", func() error {
_, _, err := st.CreateChannel(ctx, store.CreateChannelInput{})
return err
}},
{"create message", func() error {
_, _, err := st.CreateMessage(ctx, store.CreateMessageInput{})
return err
}},
{"create reply", func() error {
_, _, _, err := st.CreateThreadReply(ctx, store.CreateThreadReplyInput{})
return err
}},
{"add reaction", func() error {
_, err := st.AddReaction(ctx, store.CreateReactionInput{})
return err
}},
{"remove reaction", func() error {
_, err := st.RemoveReaction(ctx, store.CreateReactionInput{})
return err
}},
{"create upload", func() error {
_, err := st.CreateUpload(ctx, store.CreateUploadInput{})
return err
}},
{"attach upload", func() error {
return st.AttachUpload(ctx, store.AttachUploadInput{})
}},
{"create dm", func() error {
_, err := st.CreateDirectConversation(ctx, store.CreateDirectConversationInput{})
return err
}},
{"create dm message", func() error {
_, _, err := st.CreateDirectMessage(ctx, store.CreateDirectMessageInput{})
return err
}},
{"magic link", func() error {
_, err := st.CreateMagicLink(ctx, "x@example.com", "x")
return err
}},
{"identity", func() error {
_, err := st.UpsertIdentityUser(ctx, store.UpsertIdentityUserInput{Provider: "github", ProviderSubject: "1"})
return err
}},
{"session", func() error {
_, err := st.CreateSession(ctx, "usr_missing")
return err
}},
}
for _, tc := range errorCases {
t.Run(tc.name, func(t *testing.T) {
if err := tc.fn(); err == nil {
t.Fatal("expected closed database error")
}
})
}
}
func newTestStore(t *testing.T) *Store {
t.Helper()
st, err := Open("sqlite://" + filepath.Join(t.TempDir(), "clickclack.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = st.Close() })
if err := st.Migrate(context.Background()); err != nil {
t.Fatal(err)
}
return st
}