233 lines
6.9 KiB
Go
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
|
|
}
|