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

336 lines
12 KiB
Go

package sqlite
import (
"context"
"testing"
"github.com/openclaw/clickclack/apps/api/internal/store"
)
func TestStoreFaultBranches(t *testing.T) {
t.Parallel()
t.Run("event insert failure", func(t *testing.T) {
t.Parallel()
ctx, st, owner, workspace, _ := seededStore(t)
if _, err := st.db.ExecContext(ctx, `DROP TABLE events`); err != nil {
t.Fatal(err)
}
if _, _, err := st.CreateChannel(ctx, store.CreateChannelInput{WorkspaceID: workspace.ID, UserID: owner.ID, Name: "events-down"}); err == nil {
t.Fatal("expected event insert failure")
}
})
t.Run("thread state failures", func(t *testing.T) {
t.Parallel()
ctx, st, owner, _, channel := seededStore(t)
root, _, err := st.CreateMessage(ctx, store.CreateMessageInput{ChannelID: channel.ID, AuthorID: owner.ID, Body: "root"})
if err != nil {
t.Fatal(err)
}
if _, err := st.db.ExecContext(ctx, `DROP TABLE thread_state`); err != nil {
t.Fatal(err)
}
if _, _, _, err := st.GetThread(ctx, root.ID, owner.ID, 10); err == nil {
t.Fatal("expected get thread state failure")
}
if _, _, _, err := st.CreateThreadReply(ctx, store.CreateThreadReplyInput{RootMessageID: root.ID, AuthorID: owner.ID, Body: "reply"}); err == nil {
t.Fatal("expected update thread state failure")
}
})
t.Run("attachment hydration failure", func(t *testing.T) {
t.Parallel()
ctx, st, owner, _, channel := seededStore(t)
if _, _, err := st.CreateMessage(ctx, store.CreateMessageInput{ChannelID: channel.ID, AuthorID: owner.ID, Body: "root"}); err != nil {
t.Fatal(err)
}
if _, err := st.db.ExecContext(ctx, `DROP TABLE message_attachments`); err != nil {
t.Fatal(err)
}
if _, err := st.ListMessages(ctx, channel.ID, owner.ID, 0, 10); err == nil {
t.Fatal("expected attachment hydration failure")
}
})
t.Run("direct conversation query failure", func(t *testing.T) {
t.Parallel()
ctx, st, owner, workspace, _ := seededStore(t)
if _, err := st.db.ExecContext(ctx, `DROP TABLE direct_conversation_members`); err != nil {
t.Fatal(err)
}
if _, err := st.ListDirectConversations(ctx, workspace.ID, owner.ID); err == nil {
t.Fatal("expected direct conversation query failure")
}
if _, err := st.CreateDirectConversation(ctx, store.CreateDirectConversationInput{WorkspaceID: workspace.ID, UserID: owner.ID, MemberIDs: []string{owner.ID}}); err == nil {
t.Fatal("expected direct conversation membership failure")
}
})
t.Run("direct member hydration failure", func(t *testing.T) {
t.Parallel()
ctx, st, owner, workspace, _ := seededStore(t)
member, err := st.CreateUser(ctx, store.CreateUserInput{DisplayName: "Member", Email: "member-hydrate@example.com"})
if err != nil {
t.Fatal(err)
}
if err := st.AddWorkspaceMember(ctx, workspace.ID, member.ID, "member"); err != nil {
t.Fatal(err)
}
if _, err := st.CreateDirectConversation(ctx, store.CreateDirectConversationInput{WorkspaceID: workspace.ID, UserID: owner.ID, MemberIDs: []string{member.ID}}); err != nil {
t.Fatal(err)
}
if _, err := st.db.ExecContext(ctx, `DROP TABLE users`); err != nil {
t.Fatal(err)
}
if _, err := st.ListDirectConversations(ctx, workspace.ID, owner.ID); err == nil {
t.Fatal("expected direct member hydration failure")
}
})
t.Run("upload query failure", func(t *testing.T) {
t.Parallel()
ctx, st, owner, workspace, channel := seededStore(t)
root, _, err := st.CreateMessage(ctx, store.CreateMessageInput{ChannelID: channel.ID, AuthorID: owner.ID, Body: "root"})
if err != nil {
t.Fatal(err)
}
upload, err := st.CreateUpload(ctx, store.CreateUploadInput{WorkspaceID: workspace.ID, OwnerID: owner.ID, Filename: "x", ContentType: "text/plain", ByteSize: 1, StoragePath: "/tmp/x"})
if err != nil {
t.Fatal(err)
}
if _, err := st.db.ExecContext(ctx, `DROP TABLE uploads`); err != nil {
t.Fatal(err)
}
if _, err := st.GetUpload(ctx, upload.ID, owner.ID); err == nil {
t.Fatal("expected get upload failure")
}
if err := st.AttachUpload(ctx, store.AttachUploadInput{MessageID: root.ID, UploadID: upload.ID, UserID: owner.ID}); err == nil {
t.Fatal("expected attach upload failure")
}
})
t.Run("bad magic link expiration", func(t *testing.T) {
t.Parallel()
ctx, st, _, _, _ := seededStore(t)
link, err := st.CreateMagicLink(ctx, "bad-expiry@example.com", "Bad")
if err != nil {
t.Fatal(err)
}
if _, err := st.db.ExecContext(ctx, `UPDATE auth_magic_links SET expires_at = 'bad' WHERE token = ?`, link.Token); err != nil {
t.Fatal(err)
}
if _, _, err := st.ConsumeMagicLink(ctx, link.Token); err == nil {
t.Fatal("expected bad expiry error")
}
if _, _, err := st.ConsumeMagicLink(ctx, "missing"); err == nil {
t.Fatal("expected missing link error")
}
})
t.Run("magic link session failure", func(t *testing.T) {
t.Parallel()
ctx, st, _, _, _ := seededStore(t)
link, err := st.CreateMagicLink(ctx, "session-fail@example.com", "Session")
if err != nil {
t.Fatal(err)
}
if _, err := st.db.ExecContext(ctx, `DROP TABLE sessions`); err != nil {
t.Fatal(err)
}
if _, _, err := st.ConsumeMagicLink(ctx, link.Token); err == nil {
t.Fatal("expected session create failure")
}
})
t.Run("duplicate local identity", func(t *testing.T) {
t.Parallel()
ctx, st, _, _, _ := seededStore(t)
if _, err := st.CreateUser(ctx, store.CreateUserInput{DisplayName: "One", Email: "dupe@example.com"}); err != nil {
t.Fatal(err)
}
if _, err := st.CreateUser(ctx, store.CreateUserInput{DisplayName: "Two", Email: "dupe@example.com"}); err == nil {
t.Fatal("expected duplicate identity error")
}
})
t.Run("message table failures", func(t *testing.T) {
t.Parallel()
ctx, st, owner, _, channel := seededStore(t)
if _, err := st.db.ExecContext(ctx, `DROP TABLE messages`); err != nil {
t.Fatal(err)
}
if _, err := st.ListMessages(ctx, channel.ID, owner.ID, 0, 10); err == nil {
t.Fatal("expected list messages failure")
}
if _, _, err := st.CreateMessage(ctx, store.CreateMessageInput{ChannelID: channel.ID, AuthorID: owner.ID, Body: "x"}); err == nil {
t.Fatal("expected create message sequence failure")
}
if _, _, _, err := st.GetThread(ctx, "msg_missing", owner.ID, 10); err == nil {
t.Fatal("expected get thread message failure")
}
})
t.Run("message transaction failures", func(t *testing.T) {
t.Parallel()
ctx, st, owner, _, channel := seededStore(t)
if _, err := st.db.ExecContext(ctx, `DROP TABLE thread_state`); err != nil {
t.Fatal(err)
}
if _, _, err := st.CreateMessage(ctx, store.CreateMessageInput{ChannelID: channel.ID, AuthorID: owner.ID, Body: "x"}); err == nil {
t.Fatal("expected message thread-state failure")
}
})
t.Run("message event failure", func(t *testing.T) {
t.Parallel()
ctx, st, owner, workspace, channel := seededStore(t)
if _, err := st.db.ExecContext(ctx, `DROP TABLE events`); err != nil {
t.Fatal(err)
}
if _, _, err := st.CreateMessage(ctx, store.CreateMessageInput{ChannelID: channel.ID, AuthorID: owner.ID, Body: "x"}); err == nil {
t.Fatal("expected message event failure")
}
if _, err := st.ListEventsAfter(ctx, workspace.ID, owner.ID, "", 10); err == nil {
t.Fatal("expected list events failure")
}
})
t.Run("channel and workspace write failures", func(t *testing.T) {
t.Parallel()
ctx, st, owner, workspace, _ := seededStore(t)
outsider, err := st.CreateUser(ctx, store.CreateUserInput{DisplayName: "Outsider", Email: "outsider@example.com"})
if err != nil {
t.Fatal(err)
}
if _, _, err := st.CreateChannel(ctx, store.CreateChannelInput{WorkspaceID: workspace.ID, UserID: outsider.ID, Name: "denied"}); err == nil {
t.Fatal("expected channel membership failure")
}
if _, err := st.CreateWorkspace(ctx, store.CreateWorkspaceInput{Name: "No Owner"}, "usr_missing"); err == nil {
t.Fatal("expected workspace member foreign-key failure")
}
if _, err := st.db.ExecContext(ctx, `DROP TABLE channels`); err != nil {
t.Fatal(err)
}
if _, err := st.ListChannels(ctx, workspace.ID, owner.ID); err == nil {
t.Fatal("expected list channels failure")
}
})
t.Run("reaction failures", func(t *testing.T) {
t.Parallel()
ctx, st, owner, _, channel := seededStore(t)
root, _, err := st.CreateMessage(ctx, store.CreateMessageInput{ChannelID: channel.ID, AuthorID: owner.ID, Body: "root"})
if err != nil {
t.Fatal(err)
}
outsider, err := st.CreateUser(ctx, store.CreateUserInput{DisplayName: "Outsider", Email: "out@example.com"})
if err != nil {
t.Fatal(err)
}
if _, err := st.AddReaction(ctx, store.CreateReactionInput{MessageID: root.ID, UserID: outsider.ID, Emoji: "x"}); err == nil {
t.Fatal("expected reaction membership failure")
}
if _, err := st.db.ExecContext(ctx, `DROP TABLE reactions`); err != nil {
t.Fatal(err)
}
if _, err := st.AddReaction(ctx, store.CreateReactionInput{MessageID: root.ID, UserID: owner.ID, Emoji: "x"}); err == nil {
t.Fatal("expected reaction write failure")
}
})
t.Run("direct message failures", func(t *testing.T) {
t.Parallel()
ctx, st, owner, workspace, _ := seededStore(t)
member, err := st.CreateUser(ctx, store.CreateUserInput{DisplayName: "Member", Email: "member@example.com"})
if err != nil {
t.Fatal(err)
}
if err := st.AddWorkspaceMember(ctx, workspace.ID, member.ID, "member"); err != nil {
t.Fatal(err)
}
dm, err := st.CreateDirectConversation(ctx, store.CreateDirectConversationInput{WorkspaceID: workspace.ID, UserID: owner.ID, MemberIDs: []string{member.ID}})
if err != nil {
t.Fatal(err)
}
outsider, err := st.CreateUser(ctx, store.CreateUserInput{DisplayName: "Outsider", Email: "dmout@example.com"})
if err != nil {
t.Fatal(err)
}
if _, _, err := st.CreateDirectMessage(ctx, store.CreateDirectMessageInput{ConversationID: dm.ID, AuthorID: outsider.ID, Body: "x"}); err == nil {
t.Fatal("expected direct message membership failure")
}
if _, err := st.db.ExecContext(ctx, `DROP TABLE messages`); err != nil {
t.Fatal(err)
}
if _, err := st.ListDirectMessages(ctx, dm.ID, owner.ID, 0, 10); err == nil {
t.Fatal("expected direct list failure")
}
if _, _, err := st.CreateDirectMessage(ctx, store.CreateDirectMessageInput{ConversationID: dm.ID, AuthorID: owner.ID, Body: "x"}); err == nil {
t.Fatal("expected direct message sequence failure")
}
})
t.Run("direct message transaction failures", func(t *testing.T) {
t.Parallel()
ctx, st, owner, workspace, _ := seededStore(t)
member, err := st.CreateUser(ctx, store.CreateUserInput{DisplayName: "Member", Email: "direct-tx@example.com"})
if err != nil {
t.Fatal(err)
}
if err := st.AddWorkspaceMember(ctx, workspace.ID, member.ID, "member"); err != nil {
t.Fatal(err)
}
dm, err := st.CreateDirectConversation(ctx, store.CreateDirectConversationInput{WorkspaceID: workspace.ID, UserID: owner.ID, MemberIDs: []string{member.ID}})
if err != nil {
t.Fatal(err)
}
if _, err := st.db.ExecContext(ctx, `DROP TABLE thread_state`); err != nil {
t.Fatal(err)
}
if _, _, err := st.CreateDirectMessage(ctx, store.CreateDirectMessageInput{ConversationID: dm.ID, AuthorID: owner.ID, Body: "x"}); err == nil {
t.Fatal("expected direct thread-state failure")
}
})
t.Run("direct message event failure", func(t *testing.T) {
t.Parallel()
ctx, st, owner, workspace, _ := seededStore(t)
member, err := st.CreateUser(ctx, store.CreateUserInput{DisplayName: "Member", Email: "direct-event@example.com"})
if err != nil {
t.Fatal(err)
}
if err := st.AddWorkspaceMember(ctx, workspace.ID, member.ID, "member"); err != nil {
t.Fatal(err)
}
dm, err := st.CreateDirectConversation(ctx, store.CreateDirectConversationInput{WorkspaceID: workspace.ID, UserID: owner.ID, MemberIDs: []string{member.ID}})
if err != nil {
t.Fatal(err)
}
if _, err := st.db.ExecContext(ctx, `DROP TABLE events`); err != nil {
t.Fatal(err)
}
if _, _, err := st.CreateDirectMessage(ctx, store.CreateDirectMessageInput{ConversationID: dm.ID, AuthorID: owner.ID, Body: "x"}); err == nil {
t.Fatal("expected direct event failure")
}
})
}
func seededStore(t *testing.T) (context.Context, *Store, store.User, store.Workspace, store.Channel) {
t.Helper()
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)
}
channels, err := st.ListChannels(ctx, workspaces[0].ID, owner.ID)
if err != nil {
t.Fatal(err)
}
return ctx, st, owner, workspaces[0], channels[0]
}